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