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