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