001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2024 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; 035import java.util.stream.Collectors; 036 037import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 038import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 039import com.puppycrawl.tools.checkstyle.api.DetailAST; 040import com.puppycrawl.tools.checkstyle.api.FullIdent; 041import com.puppycrawl.tools.checkstyle.api.TokenTypes; 042import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 043import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 044 045/** 046 * Base class for coupling calculation. 047 * 048 */ 049@FileStatefulCheck 050public abstract class AbstractClassCouplingCheck extends AbstractCheck { 051 052 /** A package separator - ".". */ 053 private static final char DOT = '.'; 054 055 /** Class names to ignore. */ 056 private static final Set<String> DEFAULT_EXCLUDED_CLASSES = Set.of( 057 // reserved type name 058 "var", 059 // primitives 060 "boolean", "byte", "char", "double", "float", "int", 061 "long", "short", "void", 062 // wrappers 063 "Boolean", "Byte", "Character", "Double", "Float", 064 "Integer", "Long", "Short", "Void", 065 // java.lang.* 066 "Object", "Class", 067 "String", "StringBuffer", "StringBuilder", 068 // Exceptions 069 "ArrayIndexOutOfBoundsException", "Exception", 070 "RuntimeException", "IllegalArgumentException", 071 "IllegalStateException", "IndexOutOfBoundsException", 072 "NullPointerException", "Throwable", "SecurityException", 073 "UnsupportedOperationException", 074 // java.util.* 075 "List", "ArrayList", "Deque", "Queue", "LinkedList", 076 "Set", "HashSet", "SortedSet", "TreeSet", 077 "Map", "HashMap", "SortedMap", "TreeMap", 078 "Override", "Deprecated", "SafeVarargs", "SuppressWarnings", "FunctionalInterface", 079 "Collection", "EnumSet", "LinkedHashMap", "LinkedHashSet", "Optional", 080 "OptionalDouble", "OptionalInt", "OptionalLong", 081 // java.util.stream.* 082 "DoubleStream", "IntStream", "LongStream", "Stream" 083 ); 084 085 /** Package names to ignore. */ 086 private static final Set<String> DEFAULT_EXCLUDED_PACKAGES = Collections.emptySet(); 087 088 /** Pattern to match brackets in a full type name. */ 089 private static final Pattern BRACKET_PATTERN = Pattern.compile("\\[[^]]*]"); 090 091 /** Specify user-configured regular expressions to ignore classes. */ 092 private final List<Pattern> excludeClassesRegexps = new ArrayList<>(); 093 094 /** A map of (imported class name -> class name with package) pairs. */ 095 private final Map<String, String> importedClassPackages = new HashMap<>(); 096 097 /** Stack of class contexts. */ 098 private final Deque<ClassContext> classesContexts = new ArrayDeque<>(); 099 100 /** Specify user-configured class names to ignore. */ 101 private Set<String> excludedClasses = DEFAULT_EXCLUDED_CLASSES; 102 103 /** 104 * Specify user-configured packages to ignore. 105 */ 106 private Set<String> excludedPackages = DEFAULT_EXCLUDED_PACKAGES; 107 108 /** Specify the maximum threshold allowed. */ 109 private int max; 110 111 /** Current file package. */ 112 private String packageName; 113 114 /** 115 * Creates new instance of the check. 116 * 117 * @param defaultMax default value for allowed complexity. 118 */ 119 protected AbstractClassCouplingCheck(int defaultMax) { 120 max = defaultMax; 121 excludeClassesRegexps.add(CommonUtil.createPattern("^$")); 122 } 123 124 /** 125 * Returns message key we use for log violations. 126 * 127 * @return message key we use for log violations. 128 */ 129 protected abstract String getLogMessageId(); 130 131 @Override 132 public final int[] getDefaultTokens() { 133 return getRequiredTokens(); 134 } 135 136 /** 137 * Setter to specify the maximum threshold allowed. 138 * 139 * @param max allowed complexity. 140 */ 141 public final void setMax(int max) { 142 this.max = max; 143 } 144 145 /** 146 * Setter to specify user-configured class names to ignore. 147 * 148 * @param excludedClasses classes to ignore. 149 */ 150 public final void setExcludedClasses(String... excludedClasses) { 151 this.excludedClasses = Set.of(excludedClasses); 152 } 153 154 /** 155 * Setter to specify user-configured regular expressions to ignore classes. 156 * 157 * @param from array representing regular expressions of classes to ignore. 158 */ 159 public void setExcludeClassesRegexps(Pattern... from) { 160 excludeClassesRegexps.addAll(Arrays.asList(from)); 161 } 162 163 /** 164 * Setter to specify user-configured packages to ignore. 165 * 166 * @param excludedPackages packages to ignore. 167 * @throws IllegalArgumentException if there are invalid identifiers among the packages. 168 */ 169 public final void setExcludedPackages(String... excludedPackages) { 170 final List<String> invalidIdentifiers = Arrays.stream(excludedPackages) 171 .filter(Predicate.not(CommonUtil::isName)) 172 .collect(Collectors.toUnmodifiableList()); 173 if (!invalidIdentifiers.isEmpty()) { 174 throw new IllegalArgumentException( 175 "the following values are not valid identifiers: " + invalidIdentifiers); 176 } 177 178 this.excludedPackages = Set.of(excludedPackages); 179 } 180 181 @Override 182 public final void beginTree(DetailAST ast) { 183 importedClassPackages.clear(); 184 classesContexts.clear(); 185 classesContexts.push(new ClassContext("", null)); 186 packageName = ""; 187 } 188 189 @Override 190 public void visitToken(DetailAST ast) { 191 switch (ast.getType()) { 192 case TokenTypes.PACKAGE_DEF: 193 visitPackageDef(ast); 194 break; 195 case TokenTypes.IMPORT: 196 registerImport(ast); 197 break; 198 case TokenTypes.CLASS_DEF: 199 case TokenTypes.INTERFACE_DEF: 200 case TokenTypes.ANNOTATION_DEF: 201 case TokenTypes.ENUM_DEF: 202 case TokenTypes.RECORD_DEF: 203 visitClassDef(ast); 204 break; 205 case TokenTypes.EXTENDS_CLAUSE: 206 case TokenTypes.IMPLEMENTS_CLAUSE: 207 case TokenTypes.TYPE: 208 visitType(ast); 209 break; 210 case TokenTypes.LITERAL_NEW: 211 visitLiteralNew(ast); 212 break; 213 case TokenTypes.LITERAL_THROWS: 214 visitLiteralThrows(ast); 215 break; 216 case TokenTypes.ANNOTATION: 217 visitAnnotationType(ast); 218 break; 219 default: 220 throw new IllegalArgumentException("Unknown type: " + ast); 221 } 222 } 223 224 @Override 225 public void leaveToken(DetailAST ast) { 226 if (TokenUtil.isTypeDeclaration(ast.getType())) { 227 leaveClassDef(); 228 } 229 } 230 231 /** 232 * Stores package of current class we check. 233 * 234 * @param pkg package definition. 235 */ 236 private void visitPackageDef(DetailAST pkg) { 237 final FullIdent ident = FullIdent.createFullIdent(pkg.getLastChild().getPreviousSibling()); 238 packageName = ident.getText(); 239 } 240 241 /** 242 * Creates new context for a given class. 243 * 244 * @param classDef class definition node. 245 */ 246 private void visitClassDef(DetailAST classDef) { 247 final String className = classDef.findFirstToken(TokenTypes.IDENT).getText(); 248 createNewClassContext(className, classDef); 249 } 250 251 /** Restores previous context. */ 252 private void leaveClassDef() { 253 checkCurrentClassAndRestorePrevious(); 254 } 255 256 /** 257 * Registers given import. This allows us to track imported classes. 258 * 259 * @param imp import definition. 260 */ 261 private void registerImport(DetailAST imp) { 262 final FullIdent ident = FullIdent.createFullIdent( 263 imp.getLastChild().getPreviousSibling()); 264 final String fullName = ident.getText(); 265 final int lastDot = fullName.lastIndexOf(DOT); 266 importedClassPackages.put(fullName.substring(lastDot + 1), fullName); 267 } 268 269 /** 270 * Creates new inner class context with given name and location. 271 * 272 * @param className The class name. 273 * @param ast The class ast. 274 */ 275 private void createNewClassContext(String className, DetailAST ast) { 276 classesContexts.push(new ClassContext(className, ast)); 277 } 278 279 /** Restores previous context. */ 280 private void checkCurrentClassAndRestorePrevious() { 281 classesContexts.pop().checkCoupling(); 282 } 283 284 /** 285 * Visits type token for the current class context. 286 * 287 * @param ast TYPE token. 288 */ 289 private void visitType(DetailAST ast) { 290 classesContexts.peek().visitType(ast); 291 } 292 293 /** 294 * Visits NEW token for the current class context. 295 * 296 * @param ast NEW token. 297 */ 298 private void visitLiteralNew(DetailAST ast) { 299 classesContexts.peek().visitLiteralNew(ast); 300 } 301 302 /** 303 * Visits THROWS token for the current class context. 304 * 305 * @param ast THROWS token. 306 */ 307 private void visitLiteralThrows(DetailAST ast) { 308 classesContexts.peek().visitLiteralThrows(ast); 309 } 310 311 /** 312 * Visit ANNOTATION literal and get its type to referenced classes of context. 313 * 314 * @param annotationAST Annotation ast. 315 */ 316 private void visitAnnotationType(DetailAST annotationAST) { 317 final DetailAST children = annotationAST.getFirstChild(); 318 final DetailAST type = children.getNextSibling(); 319 classesContexts.peek().addReferencedClassName(type.getText()); 320 } 321 322 /** 323 * Encapsulates information about class coupling. 324 * 325 */ 326 private final class ClassContext { 327 328 /** 329 * Set of referenced classes. 330 * Sorted by name for predictable violation messages in unit tests. 331 */ 332 private final Set<String> referencedClassNames = new TreeSet<>(); 333 /** Own class name. */ 334 private final String className; 335 /* Location of own class. (Used to log violations) */ 336 /** AST of class definition. */ 337 private final DetailAST classAst; 338 339 /** 340 * Create new context associated with given class. 341 * 342 * @param className name of the given class. 343 * @param ast ast of class definition. 344 */ 345 private ClassContext(String className, DetailAST ast) { 346 this.className = className; 347 classAst = ast; 348 } 349 350 /** 351 * Visits throws clause and collects all exceptions we throw. 352 * 353 * @param literalThrows throws to process. 354 */ 355 public void visitLiteralThrows(DetailAST literalThrows) { 356 for (DetailAST childAST = literalThrows.getFirstChild(); 357 childAST != null; 358 childAST = childAST.getNextSibling()) { 359 if (childAST.getType() != TokenTypes.COMMA) { 360 addReferencedClassName(childAST); 361 } 362 } 363 } 364 365 /** 366 * Visits type. 367 * 368 * @param ast type to process. 369 */ 370 public void visitType(DetailAST ast) { 371 DetailAST child = ast.getFirstChild(); 372 while (child != null) { 373 if (TokenUtil.isOfType(child, TokenTypes.IDENT, TokenTypes.DOT)) { 374 final String fullTypeName = FullIdent.createFullIdent(child).getText(); 375 final String trimmed = BRACKET_PATTERN 376 .matcher(fullTypeName).replaceAll(""); 377 addReferencedClassName(trimmed); 378 } 379 child = child.getNextSibling(); 380 } 381 } 382 383 /** 384 * Visits NEW. 385 * 386 * @param ast NEW to process. 387 */ 388 public void visitLiteralNew(DetailAST ast) { 389 addReferencedClassName(ast.getFirstChild()); 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 public 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}