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 compilation unit (JEP 512). 238 * 239 * @param compactCompilationUnit COMPACT_COMPILATION_UNIT node. 240 */ 241 private void visitCompactCompilationUnit(DetailAST compactCompilationUnit) { 242 createNewClassContext("", compactCompilationUnit); 243 } 244 245 /** Restores previous context. */ 246 private void leaveClassDef() { 247 checkCurrentClassAndRestorePrevious(); 248 } 249 250 /** 251 * Registers given import. This allows us to track imported classes. 252 * 253 * @param imp import definition. 254 */ 255 private void registerImport(DetailAST imp) { 256 final FullIdent ident = FullIdent.createFullIdent( 257 imp.getLastChild().getPreviousSibling()); 258 final String fullName = ident.getText(); 259 final int lastDot = fullName.lastIndexOf(DOT); 260 importedClassPackages.put(fullName.substring(lastDot + 1), fullName); 261 } 262 263 /** 264 * Creates new inner class context with given name and location. 265 * 266 * @param className The class name. 267 * @param ast The class ast. 268 */ 269 private void createNewClassContext(String className, DetailAST ast) { 270 classesContexts.push(new ClassContext(className, ast)); 271 } 272 273 /** Restores previous context. */ 274 private void checkCurrentClassAndRestorePrevious() { 275 classesContexts.pop().checkCoupling(); 276 } 277 278 /** 279 * Visits type token for the current class context. 280 * 281 * @param ast TYPE token. 282 */ 283 private void visitType(DetailAST ast) { 284 classesContexts.peek().visitType(ast); 285 } 286 287 /** 288 * Visits NEW token for the current class context. 289 * 290 * @param ast NEW token. 291 */ 292 private void visitLiteralNew(DetailAST ast) { 293 classesContexts.peek().visitLiteralNew(ast); 294 } 295 296 /** 297 * Visits THROWS token for the current class context. 298 * 299 * @param ast THROWS token. 300 */ 301 private void visitLiteralThrows(DetailAST ast) { 302 classesContexts.peek().visitLiteralThrows(ast); 303 } 304 305 /** 306 * Visit ANNOTATION literal and get its type to referenced classes of context. 307 * 308 * @param annotationAST Annotation ast. 309 */ 310 private void visitAnnotationType(DetailAST annotationAST) { 311 final DetailAST children = annotationAST.getFirstChild(); 312 final DetailAST type = children.getNextSibling(); 313 classesContexts.peek().addReferencedClassName(type.getText()); 314 } 315 316 /** 317 * Encapsulates information about class coupling. 318 * 319 */ 320 private final class ClassContext { 321 322 /** 323 * Set of referenced classes. 324 * Sorted by name for predictable violation messages in unit tests. 325 */ 326 private final Set<String> referencedClassNames = new TreeSet<>(); 327 /** Own class name. */ 328 private final String className; 329 /* Location of own class. (Used to log violations) */ 330 /** AST of class definition. */ 331 private final DetailAST classAst; 332 333 /** 334 * Create new context associated with given class. 335 * 336 * @param className name of the given class. 337 * @param ast ast of class definition. 338 */ 339 private ClassContext(String className, DetailAST ast) { 340 this.className = className; 341 classAst = ast; 342 } 343 344 /** 345 * Visits throws clause and collects all exceptions we throw. 346 * 347 * @param literalThrows throws to process. 348 */ 349 /* package */ void visitLiteralThrows(DetailAST literalThrows) { 350 for (DetailAST childAST = literalThrows.getFirstChild(); 351 childAST != null; 352 childAST = childAST.getNextSibling()) { 353 if (childAST.getType() != TokenTypes.COMMA) { 354 addReferencedClassName(childAST); 355 } 356 } 357 } 358 359 /** 360 * Visits type. 361 * 362 * @param ast type to process. 363 */ 364 /* package */ void visitType(DetailAST ast) { 365 DetailAST child = ast.getFirstChild(); 366 while (child != null) { 367 if (TokenUtil.isOfType(child, TokenTypes.IDENT, TokenTypes.DOT)) { 368 final String fullTypeName = FullIdent.createFullIdent(child).getText(); 369 final String trimmed = BRACKET_PATTERN 370 .matcher(fullTypeName).replaceAll(""); 371 addReferencedClassName(trimmed); 372 } 373 child = child.getNextSibling(); 374 } 375 } 376 377 /** 378 * Visits NEW. 379 * 380 * @param ast NEW to process. 381 */ 382 /* package */ void visitLiteralNew(DetailAST ast) { 383 384 if (ast.getParent().getType() == TokenTypes.METHOD_REF) { 385 addReferencedClassName(ast.getParent().getFirstChild()); 386 } 387 else { 388 addReferencedClassName(ast); 389 } 390 } 391 392 /** 393 * Adds new referenced class. 394 * 395 * @param ast a node which represents referenced class. 396 */ 397 private void addReferencedClassName(DetailAST ast) { 398 final String fullIdentName = FullIdent.createFullIdent(ast).getText(); 399 final String trimmed = BRACKET_PATTERN 400 .matcher(fullIdentName).replaceAll(""); 401 addReferencedClassName(trimmed); 402 } 403 404 /** 405 * Adds new referenced class. 406 * 407 * @param referencedClassName class name of the referenced class. 408 */ 409 private void addReferencedClassName(String referencedClassName) { 410 if (isSignificant(referencedClassName)) { 411 referencedClassNames.add(referencedClassName); 412 } 413 } 414 415 /** Checks if coupling less than allowed or not. */ 416 /* package */ void checkCoupling() { 417 referencedClassNames.remove(className); 418 referencedClassNames.remove(packageName + DOT + className); 419 420 if (referencedClassNames.size() > max) { 421 log(classAst, getLogMessageId(), 422 referencedClassNames.size(), max, 423 referencedClassNames.toString()); 424 } 425 } 426 427 /** 428 * Checks if given class shouldn't be ignored and not from java.lang. 429 * 430 * @param candidateClassName class to check. 431 * @return true if we should count this class. 432 */ 433 private boolean isSignificant(String candidateClassName) { 434 return !excludedClasses.contains(candidateClassName) 435 && !isFromExcludedPackage(candidateClassName) 436 && !isExcludedClassRegexp(candidateClassName); 437 } 438 439 /** 440 * Checks if given class should be ignored as it belongs to excluded package. 441 * 442 * @param candidateClassName class to check 443 * @return true if we should not count this class. 444 */ 445 private boolean isFromExcludedPackage(String candidateClassName) { 446 String classNameWithPackage = candidateClassName; 447 if (candidateClassName.indexOf(DOT) == -1) { 448 classNameWithPackage = getClassNameWithPackage(candidateClassName) 449 .orElse(""); 450 } 451 boolean isFromExcludedPackage = false; 452 if (classNameWithPackage.indexOf(DOT) != -1) { 453 final int lastDotIndex = classNameWithPackage.lastIndexOf(DOT); 454 final String candidatePackageName = 455 classNameWithPackage.substring(0, lastDotIndex); 456 isFromExcludedPackage = candidatePackageName.startsWith("java.lang") 457 || excludedPackages.contains(candidatePackageName); 458 } 459 return isFromExcludedPackage; 460 } 461 462 /** 463 * Retrieves class name with packages. Uses previously registered imports to 464 * get the full class name. 465 * 466 * @param examineClassName Class name to be retrieved. 467 * @return Class name with package name, if found, {@link Optional#empty()} otherwise. 468 */ 469 private Optional<String> getClassNameWithPackage(String examineClassName) { 470 return Optional.ofNullable(importedClassPackages.get(examineClassName)); 471 } 472 473 /** 474 * Checks if given class should be ignored as it belongs to excluded class regexp. 475 * 476 * @param candidateClassName class to check. 477 * @return true if we should not count this class. 478 */ 479 private boolean isExcludedClassRegexp(String candidateClassName) { 480 boolean result = false; 481 for (Pattern pattern : excludeClassesRegexps) { 482 if (pattern.matcher(candidateClassName).matches()) { 483 result = true; 484 break; 485 } 486 } 487 return result; 488 } 489 490 } 491 492}