001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2022 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. All excluded packages 105 * should end with a period, so it also appends a dot to a package name. 106 */ 107 private Set<String> excludedPackages = DEFAULT_EXCLUDED_PACKAGES; 108 109 /** Specify the maximum threshold allowed. */ 110 private int max; 111 112 /** Current file package. */ 113 private String packageName; 114 115 /** 116 * Creates new instance of the check. 117 * 118 * @param defaultMax default value for allowed complexity. 119 */ 120 protected AbstractClassCouplingCheck(int defaultMax) { 121 max = defaultMax; 122 excludeClassesRegexps.add(CommonUtil.createPattern("^$")); 123 } 124 125 /** 126 * Returns message key we use for log violations. 127 * 128 * @return message key we use for log violations. 129 */ 130 protected abstract String getLogMessageId(); 131 132 @Override 133 public final int[] getDefaultTokens() { 134 return getRequiredTokens(); 135 } 136 137 /** 138 * Setter to specify the maximum threshold allowed. 139 * 140 * @param max allowed complexity. 141 */ 142 public final void setMax(int max) { 143 this.max = max; 144 } 145 146 /** 147 * Setter to specify user-configured class names to ignore. 148 * 149 * @param excludedClasses classes to ignore. 150 */ 151 public final void setExcludedClasses(String... excludedClasses) { 152 this.excludedClasses = Set.of(excludedClasses); 153 } 154 155 /** 156 * Setter to specify user-configured regular expressions to ignore classes. 157 * 158 * @param from array representing regular expressions of classes to ignore. 159 */ 160 public void setExcludeClassesRegexps(String... from) { 161 Arrays.stream(from) 162 .map(CommonUtil::createPattern) 163 .distinct() 164 .forEach(excludeClassesRegexps::add); 165 } 166 167 /** 168 * Setter to specify user-configured packages to ignore. All excluded packages 169 * should end with a period, so it also appends a dot to a package name. 170 * 171 * @param excludedPackages packages to ignore. 172 * @throws IllegalArgumentException if there are invalid identifiers among the packages. 173 */ 174 public final void setExcludedPackages(String... excludedPackages) { 175 final List<String> invalidIdentifiers = Arrays.stream(excludedPackages) 176 .filter(Predicate.not(CommonUtil::isName)) 177 .collect(Collectors.toList()); 178 if (!invalidIdentifiers.isEmpty()) { 179 throw new IllegalArgumentException( 180 "the following values are not valid identifiers: " + invalidIdentifiers); 181 } 182 183 this.excludedPackages = Set.of(excludedPackages); 184 } 185 186 @Override 187 public final void beginTree(DetailAST ast) { 188 importedClassPackages.clear(); 189 classesContexts.clear(); 190 classesContexts.push(new ClassContext("", null)); 191 packageName = ""; 192 } 193 194 @Override 195 public void visitToken(DetailAST ast) { 196 switch (ast.getType()) { 197 case TokenTypes.PACKAGE_DEF: 198 visitPackageDef(ast); 199 break; 200 case TokenTypes.IMPORT: 201 registerImport(ast); 202 break; 203 case TokenTypes.CLASS_DEF: 204 case TokenTypes.INTERFACE_DEF: 205 case TokenTypes.ANNOTATION_DEF: 206 case TokenTypes.ENUM_DEF: 207 case TokenTypes.RECORD_DEF: 208 visitClassDef(ast); 209 break; 210 case TokenTypes.EXTENDS_CLAUSE: 211 case TokenTypes.IMPLEMENTS_CLAUSE: 212 case TokenTypes.TYPE: 213 visitType(ast); 214 break; 215 case TokenTypes.LITERAL_NEW: 216 visitLiteralNew(ast); 217 break; 218 case TokenTypes.LITERAL_THROWS: 219 visitLiteralThrows(ast); 220 break; 221 case TokenTypes.ANNOTATION: 222 visitAnnotationType(ast); 223 break; 224 default: 225 throw new IllegalArgumentException("Unknown type: " + ast); 226 } 227 } 228 229 @Override 230 public void leaveToken(DetailAST ast) { 231 if (TokenUtil.isTypeDeclaration(ast.getType())) { 232 leaveClassDef(); 233 } 234 } 235 236 /** 237 * Stores package of current class we check. 238 * 239 * @param pkg package definition. 240 */ 241 private void visitPackageDef(DetailAST pkg) { 242 final FullIdent ident = FullIdent.createFullIdent(pkg.getLastChild().getPreviousSibling()); 243 packageName = ident.getText(); 244 } 245 246 /** 247 * Creates new context for a given class. 248 * 249 * @param classDef class definition node. 250 */ 251 private void visitClassDef(DetailAST classDef) { 252 final String className = classDef.findFirstToken(TokenTypes.IDENT).getText(); 253 createNewClassContext(className, classDef); 254 } 255 256 /** Restores previous context. */ 257 private void leaveClassDef() { 258 checkCurrentClassAndRestorePrevious(); 259 } 260 261 /** 262 * Registers given import. This allows us to track imported classes. 263 * 264 * @param imp import definition. 265 */ 266 private void registerImport(DetailAST imp) { 267 final FullIdent ident = FullIdent.createFullIdent( 268 imp.getLastChild().getPreviousSibling()); 269 final String fullName = ident.getText(); 270 final int lastDot = fullName.lastIndexOf(DOT); 271 importedClassPackages.put(fullName.substring(lastDot + 1), fullName); 272 } 273 274 /** 275 * Creates new inner class context with given name and location. 276 * 277 * @param className The class name. 278 * @param ast The class ast. 279 */ 280 private void createNewClassContext(String className, DetailAST ast) { 281 classesContexts.push(new ClassContext(className, ast)); 282 } 283 284 /** Restores previous context. */ 285 private void checkCurrentClassAndRestorePrevious() { 286 classesContexts.pop().checkCoupling(); 287 } 288 289 /** 290 * Visits type token for the current class context. 291 * 292 * @param ast TYPE token. 293 */ 294 private void visitType(DetailAST ast) { 295 classesContexts.peek().visitType(ast); 296 } 297 298 /** 299 * Visits NEW token for the current class context. 300 * 301 * @param ast NEW token. 302 */ 303 private void visitLiteralNew(DetailAST ast) { 304 classesContexts.peek().visitLiteralNew(ast); 305 } 306 307 /** 308 * Visits THROWS token for the current class context. 309 * 310 * @param ast THROWS token. 311 */ 312 private void visitLiteralThrows(DetailAST ast) { 313 classesContexts.peek().visitLiteralThrows(ast); 314 } 315 316 /** 317 * Visit ANNOTATION literal and get its type to referenced classes of context. 318 * 319 * @param annotationAST Annotation ast. 320 */ 321 private void visitAnnotationType(DetailAST annotationAST) { 322 final DetailAST children = annotationAST.getFirstChild(); 323 final DetailAST type = children.getNextSibling(); 324 classesContexts.peek().addReferencedClassName(type.getText()); 325 } 326 327 /** 328 * Encapsulates information about class coupling. 329 * 330 */ 331 private class ClassContext { 332 333 /** 334 * Set of referenced classes. 335 * Sorted by name for predictable violation messages in unit tests. 336 */ 337 private final Set<String> referencedClassNames = new TreeSet<>(); 338 /** Own class name. */ 339 private final String className; 340 /* Location of own class. (Used to log violations) */ 341 /** AST of class definition. */ 342 private final DetailAST classAst; 343 344 /** 345 * Create new context associated with given class. 346 * 347 * @param className name of the given class. 348 * @param ast ast of class definition. 349 */ 350 /* package */ ClassContext(String className, DetailAST ast) { 351 this.className = className; 352 classAst = ast; 353 } 354 355 /** 356 * Visits throws clause and collects all exceptions we throw. 357 * 358 * @param literalThrows throws to process. 359 */ 360 public void visitLiteralThrows(DetailAST literalThrows) { 361 for (DetailAST childAST = literalThrows.getFirstChild(); 362 childAST != null; 363 childAST = childAST.getNextSibling()) { 364 if (childAST.getType() != TokenTypes.COMMA) { 365 addReferencedClassName(childAST); 366 } 367 } 368 } 369 370 /** 371 * Visits type. 372 * 373 * @param ast type to process. 374 */ 375 public void visitType(DetailAST ast) { 376 DetailAST child = ast.getFirstChild(); 377 while (child != null) { 378 if (TokenUtil.isOfType(child, TokenTypes.IDENT, TokenTypes.DOT)) { 379 final String fullTypeName = FullIdent.createFullIdent(child).getText(); 380 final String trimmed = BRACKET_PATTERN 381 .matcher(fullTypeName).replaceAll(""); 382 addReferencedClassName(trimmed); 383 } 384 child = child.getNextSibling(); 385 } 386 } 387 388 /** 389 * Visits NEW. 390 * 391 * @param ast NEW to process. 392 */ 393 public void visitLiteralNew(DetailAST ast) { 394 addReferencedClassName(ast.getFirstChild()); 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 } 496 497}