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