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(String... from) { 160 Arrays.stream(from) 161 .map(CommonUtil::createPattern) 162 .forEach(excludeClassesRegexps::add); 163 } 164 165 /** 166 * Setter to specify user-configured packages to ignore. 167 * 168 * @param excludedPackages packages to ignore. 169 * @throws IllegalArgumentException if there are invalid identifiers among the packages. 170 */ 171 public final void setExcludedPackages(String... excludedPackages) { 172 final List<String> invalidIdentifiers = Arrays.stream(excludedPackages) 173 .filter(Predicate.not(CommonUtil::isName)) 174 .collect(Collectors.toUnmodifiableList()); 175 if (!invalidIdentifiers.isEmpty()) { 176 throw new IllegalArgumentException( 177 "the following values are not valid identifiers: " + invalidIdentifiers); 178 } 179 180 this.excludedPackages = Set.of(excludedPackages); 181 } 182 183 @Override 184 public final void beginTree(DetailAST ast) { 185 importedClassPackages.clear(); 186 classesContexts.clear(); 187 classesContexts.push(new ClassContext("", null)); 188 packageName = ""; 189 } 190 191 @Override 192 public void visitToken(DetailAST ast) { 193 switch (ast.getType()) { 194 case TokenTypes.PACKAGE_DEF: 195 visitPackageDef(ast); 196 break; 197 case TokenTypes.IMPORT: 198 registerImport(ast); 199 break; 200 case TokenTypes.CLASS_DEF: 201 case TokenTypes.INTERFACE_DEF: 202 case TokenTypes.ANNOTATION_DEF: 203 case TokenTypes.ENUM_DEF: 204 case TokenTypes.RECORD_DEF: 205 visitClassDef(ast); 206 break; 207 case TokenTypes.EXTENDS_CLAUSE: 208 case TokenTypes.IMPLEMENTS_CLAUSE: 209 case TokenTypes.TYPE: 210 visitType(ast); 211 break; 212 case TokenTypes.LITERAL_NEW: 213 visitLiteralNew(ast); 214 break; 215 case TokenTypes.LITERAL_THROWS: 216 visitLiteralThrows(ast); 217 break; 218 case TokenTypes.ANNOTATION: 219 visitAnnotationType(ast); 220 break; 221 default: 222 throw new IllegalArgumentException("Unknown type: " + ast); 223 } 224 } 225 226 @Override 227 public void leaveToken(DetailAST ast) { 228 if (TokenUtil.isTypeDeclaration(ast.getType())) { 229 leaveClassDef(); 230 } 231 } 232 233 /** 234 * Stores package of current class we check. 235 * 236 * @param pkg package definition. 237 */ 238 private void visitPackageDef(DetailAST pkg) { 239 final FullIdent ident = FullIdent.createFullIdent(pkg.getLastChild().getPreviousSibling()); 240 packageName = ident.getText(); 241 } 242 243 /** 244 * Creates new context for a given class. 245 * 246 * @param classDef class definition node. 247 */ 248 private void visitClassDef(DetailAST classDef) { 249 final String className = classDef.findFirstToken(TokenTypes.IDENT).getText(); 250 createNewClassContext(className, classDef); 251 } 252 253 /** Restores previous context. */ 254 private void leaveClassDef() { 255 checkCurrentClassAndRestorePrevious(); 256 } 257 258 /** 259 * Registers given import. This allows us to track imported classes. 260 * 261 * @param imp import definition. 262 */ 263 private void registerImport(DetailAST imp) { 264 final FullIdent ident = FullIdent.createFullIdent( 265 imp.getLastChild().getPreviousSibling()); 266 final String fullName = ident.getText(); 267 final int lastDot = fullName.lastIndexOf(DOT); 268 importedClassPackages.put(fullName.substring(lastDot + 1), fullName); 269 } 270 271 /** 272 * Creates new inner class context with given name and location. 273 * 274 * @param className The class name. 275 * @param ast The class ast. 276 */ 277 private void createNewClassContext(String className, DetailAST ast) { 278 classesContexts.push(new ClassContext(className, ast)); 279 } 280 281 /** Restores previous context. */ 282 private void checkCurrentClassAndRestorePrevious() { 283 classesContexts.pop().checkCoupling(); 284 } 285 286 /** 287 * Visits type token for the current class context. 288 * 289 * @param ast TYPE token. 290 */ 291 private void visitType(DetailAST ast) { 292 classesContexts.peek().visitType(ast); 293 } 294 295 /** 296 * Visits NEW token for the current class context. 297 * 298 * @param ast NEW token. 299 */ 300 private void visitLiteralNew(DetailAST ast) { 301 classesContexts.peek().visitLiteralNew(ast); 302 } 303 304 /** 305 * Visits THROWS token for the current class context. 306 * 307 * @param ast THROWS token. 308 */ 309 private void visitLiteralThrows(DetailAST ast) { 310 classesContexts.peek().visitLiteralThrows(ast); 311 } 312 313 /** 314 * Visit ANNOTATION literal and get its type to referenced classes of context. 315 * 316 * @param annotationAST Annotation ast. 317 */ 318 private void visitAnnotationType(DetailAST annotationAST) { 319 final DetailAST children = annotationAST.getFirstChild(); 320 final DetailAST type = children.getNextSibling(); 321 classesContexts.peek().addReferencedClassName(type.getText()); 322 } 323 324 /** 325 * Encapsulates information about class coupling. 326 * 327 */ 328 private final class ClassContext { 329 330 /** 331 * Set of referenced classes. 332 * Sorted by name for predictable violation messages in unit tests. 333 */ 334 private final Set<String> referencedClassNames = new TreeSet<>(); 335 /** Own class name. */ 336 private final String className; 337 /* Location of own class. (Used to log violations) */ 338 /** AST of class definition. */ 339 private final DetailAST classAst; 340 341 /** 342 * Create new context associated with given class. 343 * 344 * @param className name of the given class. 345 * @param ast ast of class definition. 346 */ 347 private ClassContext(String className, DetailAST ast) { 348 this.className = className; 349 classAst = ast; 350 } 351 352 /** 353 * Visits throws clause and collects all exceptions we throw. 354 * 355 * @param literalThrows throws to process. 356 */ 357 public void visitLiteralThrows(DetailAST literalThrows) { 358 for (DetailAST childAST = literalThrows.getFirstChild(); 359 childAST != null; 360 childAST = childAST.getNextSibling()) { 361 if (childAST.getType() != TokenTypes.COMMA) { 362 addReferencedClassName(childAST); 363 } 364 } 365 } 366 367 /** 368 * Visits type. 369 * 370 * @param ast type to process. 371 */ 372 public void visitType(DetailAST ast) { 373 DetailAST child = ast.getFirstChild(); 374 while (child != null) { 375 if (TokenUtil.isOfType(child, TokenTypes.IDENT, TokenTypes.DOT)) { 376 final String fullTypeName = FullIdent.createFullIdent(child).getText(); 377 final String trimmed = BRACKET_PATTERN 378 .matcher(fullTypeName).replaceAll(""); 379 addReferencedClassName(trimmed); 380 } 381 child = child.getNextSibling(); 382 } 383 } 384 385 /** 386 * Visits NEW. 387 * 388 * @param ast NEW to process. 389 */ 390 public void visitLiteralNew(DetailAST ast) { 391 addReferencedClassName(ast.getFirstChild()); 392 } 393 394 /** 395 * Adds new referenced class. 396 * 397 * @param ast a node which represents referenced class. 398 */ 399 private void addReferencedClassName(DetailAST ast) { 400 final String fullIdentName = FullIdent.createFullIdent(ast).getText(); 401 final String trimmed = BRACKET_PATTERN 402 .matcher(fullIdentName).replaceAll(""); 403 addReferencedClassName(trimmed); 404 } 405 406 /** 407 * Adds new referenced class. 408 * 409 * @param referencedClassName class name of the referenced class. 410 */ 411 private void addReferencedClassName(String referencedClassName) { 412 if (isSignificant(referencedClassName)) { 413 referencedClassNames.add(referencedClassName); 414 } 415 } 416 417 /** Checks if coupling less than allowed or not. */ 418 public void checkCoupling() { 419 referencedClassNames.remove(className); 420 referencedClassNames.remove(packageName + DOT + className); 421 422 if (referencedClassNames.size() > max) { 423 log(classAst, getLogMessageId(), 424 referencedClassNames.size(), max, 425 referencedClassNames.toString()); 426 } 427 } 428 429 /** 430 * Checks if given class shouldn't be ignored and not from java.lang. 431 * 432 * @param candidateClassName class to check. 433 * @return true if we should count this class. 434 */ 435 private boolean isSignificant(String candidateClassName) { 436 return !excludedClasses.contains(candidateClassName) 437 && !isFromExcludedPackage(candidateClassName) 438 && !isExcludedClassRegexp(candidateClassName); 439 } 440 441 /** 442 * Checks if given class should be ignored as it belongs to excluded package. 443 * 444 * @param candidateClassName class to check 445 * @return true if we should not count this class. 446 */ 447 private boolean isFromExcludedPackage(String candidateClassName) { 448 String classNameWithPackage = candidateClassName; 449 if (candidateClassName.indexOf(DOT) == -1) { 450 classNameWithPackage = getClassNameWithPackage(candidateClassName) 451 .orElse(""); 452 } 453 boolean isFromExcludedPackage = false; 454 if (classNameWithPackage.indexOf(DOT) != -1) { 455 final int lastDotIndex = classNameWithPackage.lastIndexOf(DOT); 456 final String candidatePackageName = 457 classNameWithPackage.substring(0, lastDotIndex); 458 isFromExcludedPackage = candidatePackageName.startsWith("java.lang") 459 || excludedPackages.contains(candidatePackageName); 460 } 461 return isFromExcludedPackage; 462 } 463 464 /** 465 * Retrieves class name with packages. Uses previously registered imports to 466 * get the full class name. 467 * 468 * @param examineClassName Class name to be retrieved. 469 * @return Class name with package name, if found, {@link Optional#empty()} otherwise. 470 */ 471 private Optional<String> getClassNameWithPackage(String examineClassName) { 472 return Optional.ofNullable(importedClassPackages.get(examineClassName)); 473 } 474 475 /** 476 * Checks if given class should be ignored as it belongs to excluded class regexp. 477 * 478 * @param candidateClassName class to check. 479 * @return true if we should not count this class. 480 */ 481 private boolean isExcludedClassRegexp(String candidateClassName) { 482 boolean result = false; 483 for (Pattern pattern : excludeClassesRegexps) { 484 if (pattern.matcher(candidateClassName).matches()) { 485 result = true; 486 break; 487 } 488 } 489 return result; 490 } 491 492 } 493 494}