001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2025 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks.metrics; 021 022import java.util.ArrayDeque; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.Collections; 026import java.util.Deque; 027import java.util.HashMap; 028import java.util.List; 029import java.util.Map; 030import java.util.Optional; 031import java.util.Set; 032import java.util.TreeSet; 033import java.util.function.Predicate; 034import java.util.regex.Pattern; 035 036import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 037import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 038import com.puppycrawl.tools.checkstyle.api.DetailAST; 039import com.puppycrawl.tools.checkstyle.api.FullIdent; 040import com.puppycrawl.tools.checkstyle.api.TokenTypes; 041import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 042import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 043 044/** 045 * Base class for coupling calculation. 046 * 047 */ 048@FileStatefulCheck 049public abstract class AbstractClassCouplingCheck extends AbstractCheck { 050 051 /** A package separator - ".". */ 052 private static final char DOT = '.'; 053 054 /** Class names to ignore. */ 055 private static final Set<String> DEFAULT_EXCLUDED_CLASSES = Set.of( 056 // reserved type name 057 "var", 058 // primitives 059 "boolean", "byte", "char", "double", "float", "int", 060 "long", "short", "void", 061 // wrappers 062 "Boolean", "Byte", "Character", "Double", "Float", 063 "Integer", "Long", "Short", "Void", 064 // java.lang.* 065 "Object", "Class", 066 "String", "StringBuffer", "StringBuilder", 067 // Exceptions 068 "ArrayIndexOutOfBoundsException", "Exception", 069 "RuntimeException", "IllegalArgumentException", 070 "IllegalStateException", "IndexOutOfBoundsException", 071 "NullPointerException", "Throwable", "SecurityException", 072 "UnsupportedOperationException", 073 // java.util.* 074 "List", "ArrayList", "Deque", "Queue", "LinkedList", 075 "Set", "HashSet", "SortedSet", "TreeSet", 076 "Map", "HashMap", "SortedMap", "TreeMap", 077 "Override", "Deprecated", "SafeVarargs", "SuppressWarnings", "FunctionalInterface", 078 "Collection", "EnumSet", "LinkedHashMap", "LinkedHashSet", "Optional", 079 "OptionalDouble", "OptionalInt", "OptionalLong", 080 // java.util.stream.* 081 "DoubleStream", "IntStream", "LongStream", "Stream" 082 ); 083 084 /** Package names to ignore. */ 085 private static final Set<String> DEFAULT_EXCLUDED_PACKAGES = Collections.emptySet(); 086 087 /** Pattern to match brackets in a full type name. */ 088 private static final Pattern BRACKET_PATTERN = Pattern.compile("\\[[^]]*]"); 089 090 /** Specify user-configured regular expressions to ignore classes. */ 091 private final List<Pattern> excludeClassesRegexps = new ArrayList<>(); 092 093 /** A map of (imported class name -> class name with package) pairs. */ 094 private final Map<String, String> importedClassPackages = new HashMap<>(); 095 096 /** Stack of class contexts. */ 097 private final Deque<ClassContext> classesContexts = new ArrayDeque<>(); 098 099 /** Specify user-configured class names to ignore. */ 100 private Set<String> excludedClasses = DEFAULT_EXCLUDED_CLASSES; 101 102 /** 103 * Specify user-configured packages to ignore. 104 */ 105 private Set<String> excludedPackages = DEFAULT_EXCLUDED_PACKAGES; 106 107 /** Specify the maximum threshold allowed. */ 108 private int max; 109 110 /** Current file package. */ 111 private String packageName; 112 113 /** 114 * Creates new instance of the check. 115 * 116 * @param defaultMax default value for allowed complexity. 117 */ 118 protected AbstractClassCouplingCheck(int defaultMax) { 119 max = defaultMax; 120 excludeClassesRegexps.add(CommonUtil.createPattern("^$")); 121 } 122 123 /** 124 * Returns message key we use for log violations. 125 * 126 * @return message key we use for log violations. 127 */ 128 protected abstract String getLogMessageId(); 129 130 @Override 131 public final int[] getDefaultTokens() { 132 return getRequiredTokens(); 133 } 134 135 /** 136 * Setter to specify the maximum threshold allowed. 137 * 138 * @param max allowed complexity. 139 */ 140 public final void setMax(int max) { 141 this.max = max; 142 } 143 144 /** 145 * Setter to specify user-configured class names to ignore. 146 * 147 * @param excludedClasses classes to ignore. 148 */ 149 public final void setExcludedClasses(String... excludedClasses) { 150 this.excludedClasses = Set.of(excludedClasses); 151 } 152 153 /** 154 * Setter to specify user-configured regular expressions to ignore classes. 155 * 156 * @param from array representing regular expressions of classes to ignore. 157 */ 158 public void setExcludeClassesRegexps(Pattern... from) { 159 excludeClassesRegexps.addAll(Arrays.asList(from)); 160 } 161 162 /** 163 * Setter to specify user-configured packages to ignore. 164 * 165 * @param excludedPackages packages to ignore. 166 * @throws IllegalArgumentException if there are invalid identifiers among the packages. 167 */ 168 public final void setExcludedPackages(String... excludedPackages) { 169 final List<String> invalidIdentifiers = Arrays.stream(excludedPackages) 170 .filter(Predicate.not(CommonUtil::isName)) 171 .toList(); 172 if (!invalidIdentifiers.isEmpty()) { 173 throw new IllegalArgumentException( 174 "the following values are not valid identifiers: " + invalidIdentifiers); 175 } 176 177 this.excludedPackages = Set.of(excludedPackages); 178 } 179 180 @Override 181 public final void beginTree(DetailAST ast) { 182 importedClassPackages.clear(); 183 classesContexts.clear(); 184 classesContexts.push(new ClassContext("", null)); 185 packageName = ""; 186 } 187 188 @Override 189 public void visitToken(DetailAST ast) { 190 switch (ast.getType()) { 191 case TokenTypes.PACKAGE_DEF: 192 visitPackageDef(ast); 193 break; 194 case TokenTypes.IMPORT: 195 registerImport(ast); 196 break; 197 case TokenTypes.CLASS_DEF: 198 case TokenTypes.INTERFACE_DEF: 199 case TokenTypes.ANNOTATION_DEF: 200 case TokenTypes.ENUM_DEF: 201 case TokenTypes.RECORD_DEF: 202 visitClassDef(ast); 203 break; 204 case TokenTypes.EXTENDS_CLAUSE: 205 case TokenTypes.IMPLEMENTS_CLAUSE: 206 case TokenTypes.TYPE: 207 visitType(ast); 208 break; 209 case TokenTypes.LITERAL_NEW: 210 visitLiteralNew(ast); 211 break; 212 case TokenTypes.LITERAL_THROWS: 213 visitLiteralThrows(ast); 214 break; 215 case TokenTypes.ANNOTATION: 216 visitAnnotationType(ast); 217 break; 218 default: 219 throw new IllegalArgumentException("Unknown type: " + ast); 220 } 221 } 222 223 @Override 224 public void leaveToken(DetailAST ast) { 225 if (TokenUtil.isTypeDeclaration(ast.getType())) { 226 leaveClassDef(); 227 } 228 } 229 230 /** 231 * Stores package of current class we check. 232 * 233 * @param pkg package definition. 234 */ 235 private void visitPackageDef(DetailAST pkg) { 236 final FullIdent ident = FullIdent.createFullIdent(pkg.getLastChild().getPreviousSibling()); 237 packageName = ident.getText(); 238 } 239 240 /** 241 * Creates new context for a given class. 242 * 243 * @param classDef class definition node. 244 */ 245 private void visitClassDef(DetailAST classDef) { 246 final String className = classDef.findFirstToken(TokenTypes.IDENT).getText(); 247 createNewClassContext(className, classDef); 248 } 249 250 /** Restores previous context. */ 251 private void leaveClassDef() { 252 checkCurrentClassAndRestorePrevious(); 253 } 254 255 /** 256 * Registers given import. This allows us to track imported classes. 257 * 258 * @param imp import definition. 259 */ 260 private void registerImport(DetailAST imp) { 261 final FullIdent ident = FullIdent.createFullIdent( 262 imp.getLastChild().getPreviousSibling()); 263 final String fullName = ident.getText(); 264 final int lastDot = fullName.lastIndexOf(DOT); 265 importedClassPackages.put(fullName.substring(lastDot + 1), fullName); 266 } 267 268 /** 269 * Creates new inner class context with given name and location. 270 * 271 * @param className The class name. 272 * @param ast The class ast. 273 */ 274 private void createNewClassContext(String className, DetailAST ast) { 275 classesContexts.push(new ClassContext(className, ast)); 276 } 277 278 /** Restores previous context. */ 279 private void checkCurrentClassAndRestorePrevious() { 280 classesContexts.pop().checkCoupling(); 281 } 282 283 /** 284 * Visits type token for the current class context. 285 * 286 * @param ast TYPE token. 287 */ 288 private void visitType(DetailAST ast) { 289 classesContexts.peek().visitType(ast); 290 } 291 292 /** 293 * Visits NEW token for the current class context. 294 * 295 * @param ast NEW token. 296 */ 297 private void visitLiteralNew(DetailAST ast) { 298 classesContexts.peek().visitLiteralNew(ast); 299 } 300 301 /** 302 * Visits THROWS token for the current class context. 303 * 304 * @param ast THROWS token. 305 */ 306 private void visitLiteralThrows(DetailAST ast) { 307 classesContexts.peek().visitLiteralThrows(ast); 308 } 309 310 /** 311 * Visit ANNOTATION literal and get its type to referenced classes of context. 312 * 313 * @param annotationAST Annotation ast. 314 */ 315 private void visitAnnotationType(DetailAST annotationAST) { 316 final DetailAST children = annotationAST.getFirstChild(); 317 final DetailAST type = children.getNextSibling(); 318 classesContexts.peek().addReferencedClassName(type.getText()); 319 } 320 321 /** 322 * Encapsulates information about class coupling. 323 * 324 */ 325 private final class ClassContext { 326 327 /** 328 * Set of referenced classes. 329 * Sorted by name for predictable violation messages in unit tests. 330 */ 331 private final Set<String> referencedClassNames = new TreeSet<>(); 332 /** Own class name. */ 333 private final String className; 334 /* Location of own class. (Used to log violations) */ 335 /** AST of class definition. */ 336 private final DetailAST classAst; 337 338 /** 339 * Create new context associated with given class. 340 * 341 * @param className name of the given class. 342 * @param ast ast of class definition. 343 */ 344 private ClassContext(String className, DetailAST ast) { 345 this.className = className; 346 classAst = ast; 347 } 348 349 /** 350 * Visits throws clause and collects all exceptions we throw. 351 * 352 * @param literalThrows throws to process. 353 */ 354 public void visitLiteralThrows(DetailAST literalThrows) { 355 for (DetailAST childAST = literalThrows.getFirstChild(); 356 childAST != null; 357 childAST = childAST.getNextSibling()) { 358 if (childAST.getType() != TokenTypes.COMMA) { 359 addReferencedClassName(childAST); 360 } 361 } 362 } 363 364 /** 365 * Visits type. 366 * 367 * @param ast type to process. 368 */ 369 public void visitType(DetailAST ast) { 370 DetailAST child = ast.getFirstChild(); 371 while (child != null) { 372 if (TokenUtil.isOfType(child, TokenTypes.IDENT, TokenTypes.DOT)) { 373 final String fullTypeName = FullIdent.createFullIdent(child).getText(); 374 final String trimmed = BRACKET_PATTERN 375 .matcher(fullTypeName).replaceAll(""); 376 addReferencedClassName(trimmed); 377 } 378 child = child.getNextSibling(); 379 } 380 } 381 382 /** 383 * Visits NEW. 384 * 385 * @param ast NEW to process. 386 */ 387 public void visitLiteralNew(DetailAST ast) { 388 389 if (ast.getParent().getType() == TokenTypes.METHOD_REF) { 390 addReferencedClassName(ast.getParent().getFirstChild()); 391 } 392 else { 393 addReferencedClassName(ast); 394 } 395 } 396 397 /** 398 * Adds new referenced class. 399 * 400 * @param ast a node which represents referenced class. 401 */ 402 private void addReferencedClassName(DetailAST ast) { 403 final String fullIdentName = FullIdent.createFullIdent(ast).getText(); 404 final String trimmed = BRACKET_PATTERN 405 .matcher(fullIdentName).replaceAll(""); 406 addReferencedClassName(trimmed); 407 } 408 409 /** 410 * Adds new referenced class. 411 * 412 * @param referencedClassName class name of the referenced class. 413 */ 414 private void addReferencedClassName(String referencedClassName) { 415 if (isSignificant(referencedClassName)) { 416 referencedClassNames.add(referencedClassName); 417 } 418 } 419 420 /** Checks if coupling less than allowed or not. */ 421 public void checkCoupling() { 422 referencedClassNames.remove(className); 423 referencedClassNames.remove(packageName + DOT + className); 424 425 if (referencedClassNames.size() > max) { 426 log(classAst, getLogMessageId(), 427 referencedClassNames.size(), max, 428 referencedClassNames.toString()); 429 } 430 } 431 432 /** 433 * Checks if given class shouldn't be ignored and not from java.lang. 434 * 435 * @param candidateClassName class to check. 436 * @return true if we should count this class. 437 */ 438 private boolean isSignificant(String candidateClassName) { 439 return !excludedClasses.contains(candidateClassName) 440 && !isFromExcludedPackage(candidateClassName) 441 && !isExcludedClassRegexp(candidateClassName); 442 } 443 444 /** 445 * Checks if given class should be ignored as it belongs to excluded package. 446 * 447 * @param candidateClassName class to check 448 * @return true if we should not count this class. 449 */ 450 private boolean isFromExcludedPackage(String candidateClassName) { 451 String classNameWithPackage = candidateClassName; 452 if (candidateClassName.indexOf(DOT) == -1) { 453 classNameWithPackage = getClassNameWithPackage(candidateClassName) 454 .orElse(""); 455 } 456 boolean isFromExcludedPackage = false; 457 if (classNameWithPackage.indexOf(DOT) != -1) { 458 final int lastDotIndex = classNameWithPackage.lastIndexOf(DOT); 459 final String candidatePackageName = 460 classNameWithPackage.substring(0, lastDotIndex); 461 isFromExcludedPackage = candidatePackageName.startsWith("java.lang") 462 || excludedPackages.contains(candidatePackageName); 463 } 464 return isFromExcludedPackage; 465 } 466 467 /** 468 * Retrieves class name with packages. Uses previously registered imports to 469 * get the full class name. 470 * 471 * @param examineClassName Class name to be retrieved. 472 * @return Class name with package name, if found, {@link Optional#empty()} otherwise. 473 */ 474 private Optional<String> getClassNameWithPackage(String examineClassName) { 475 return Optional.ofNullable(importedClassPackages.get(examineClassName)); 476 } 477 478 /** 479 * Checks if given class should be ignored as it belongs to excluded class regexp. 480 * 481 * @param candidateClassName class to check. 482 * @return true if we should not count this class. 483 */ 484 private boolean isExcludedClassRegexp(String candidateClassName) { 485 boolean result = false; 486 for (Pattern pattern : excludeClassesRegexps) { 487 if (pattern.matcher(candidateClassName).matches()) { 488 result = true; 489 break; 490 } 491 } 492 return result; 493 } 494 } 495}