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