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