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