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