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