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