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