Coverage Report - com.puppycrawl.tools.checkstyle.checks.metrics.AbstractClassCouplingCheck
 
Classes in this File Line Coverage Branch Coverage Complexity
AbstractClassCouplingCheck
100%
60/60
100%
13/13
0
AbstractClassCouplingCheck$1
N/A
N/A
0
AbstractClassCouplingCheck$ClassContext
100%
51/51
100%
26/26
0
AbstractClassCouplingCheck$FileContext
100%
28/28
100%
2/2
0
 
 1  
 ////////////////////////////////////////////////////////////////////////////////
 2  
 // checkstyle: Checks Java source code for adherence to a set of rules.
 3  
 // Copyright (C) 2001-2017 the original author or authors.
 4  
 //
 5  
 // This library is free software; you can redistribute it and/or
 6  
 // modify it under the terms of the GNU Lesser General Public
 7  
 // License as published by the Free Software Foundation; either
 8  
 // version 2.1 of the License, or (at your option) any later version.
 9  
 //
 10  
 // This library is distributed in the hope that it will be useful,
 11  
 // but WITHOUT ANY WARRANTY; without even the implied warranty of
 12  
 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 13  
 // Lesser General Public License for more details.
 14  
 //
 15  
 // You should have received a copy of the GNU Lesser General Public
 16  
 // License along with this library; if not, write to the Free Software
 17  
 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 18  
 ////////////////////////////////////////////////////////////////////////////////
 19  
 
 20  
 package com.puppycrawl.tools.checkstyle.checks.metrics;
 21  
 
 22  
 import java.util.ArrayDeque;
 23  
 import java.util.ArrayList;
 24  
 import java.util.Arrays;
 25  
 import java.util.Collections;
 26  
 import java.util.Deque;
 27  
 import java.util.HashMap;
 28  
 import java.util.List;
 29  
 import java.util.Map;
 30  
 import java.util.Optional;
 31  
 import java.util.Set;
 32  
 import java.util.TreeSet;
 33  
 import java.util.regex.Pattern;
 34  
 import java.util.stream.Collectors;
 35  
 
 36  
 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
 37  
 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
 38  
 import com.puppycrawl.tools.checkstyle.api.DetailAST;
 39  
 import com.puppycrawl.tools.checkstyle.api.FullIdent;
 40  
 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
 41  
 import com.puppycrawl.tools.checkstyle.utils.CheckUtils;
 42  
 import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
 43  
 
 44  
 /**
 45  
  * Base class for coupling calculation.
 46  
  *
 47  
  * @author <a href="mailto:simon@redhillconsulting.com.au">Simon Harris</a>
 48  
  * @author o_sukhodolsky
 49  
  */
 50  
 @FileStatefulCheck
 51  356
 public abstract class AbstractClassCouplingCheck extends AbstractCheck {
 52  
     /** A package separator - "." */
 53  
     private static final String DOT = ".";
 54  
 
 55  
     /** Class names to ignore. */
 56  2
     private static final Set<String> DEFAULT_EXCLUDED_CLASSES = Collections.unmodifiableSet(
 57  1
         Arrays.stream(new String[] {
 58  
             // primitives
 59  
             "boolean", "byte", "char", "double", "float", "int",
 60  
             "long", "short", "void",
 61  
             // wrappers
 62  
             "Boolean", "Byte", "Character", "Double", "Float",
 63  
             "Integer", "Long", "Short", "Void",
 64  
             // java.lang.*
 65  
             "Object", "Class",
 66  
             "String", "StringBuffer", "StringBuilder",
 67  
             // Exceptions
 68  
             "ArrayIndexOutOfBoundsException", "Exception",
 69  
             "RuntimeException", "IllegalArgumentException",
 70  
             "IllegalStateException", "IndexOutOfBoundsException",
 71  
             "NullPointerException", "Throwable", "SecurityException",
 72  
             "UnsupportedOperationException",
 73  
             // java.util.*
 74  
             "List", "ArrayList", "Deque", "Queue", "LinkedList",
 75  
             "Set", "HashSet", "SortedSet", "TreeSet",
 76  
             "Map", "HashMap", "SortedMap", "TreeMap",
 77  1
         }).collect(Collectors.toSet()));
 78  
 
 79  
     /** Package names to ignore. */
 80  1
     private static final Set<String> DEFAULT_EXCLUDED_PACKAGES = Collections.emptySet();
 81  
 
 82  
     /** User-configured regular expressions to ignore classes. */
 83  48
     private final List<Pattern> excludeClassesRegexps = new ArrayList<>();
 84  
 
 85  
     /** User-configured class names to ignore. */
 86  48
     private Set<String> excludedClasses = DEFAULT_EXCLUDED_CLASSES;
 87  
     /** User-configured package names to ignore. */
 88  48
     private Set<String> excludedPackages = DEFAULT_EXCLUDED_PACKAGES;
 89  
     /** Allowed complexity. */
 90  
     private int max;
 91  
 
 92  
     /** Current file context. */
 93  
     private FileContext fileContext;
 94  
 
 95  
     /**
 96  
      * Creates new instance of the check.
 97  
      * @param defaultMax default value for allowed complexity.
 98  
      */
 99  48
     protected AbstractClassCouplingCheck(int defaultMax) {
 100  48
         max = defaultMax;
 101  48
         excludeClassesRegexps.add(CommonUtils.createPattern("^$"));
 102  48
     }
 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  76
         return getRequiredTokens();
 113  
     }
 114  
 
 115  
     /**
 116  
      * Sets maximum allowed complexity.
 117  
      * @param max allowed complexity.
 118  
      */
 119  
     public final void setMax(int max) {
 120  18
         this.max = max;
 121  18
     }
 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  7
         this.excludedClasses =
 129  7
             Collections.unmodifiableSet(Arrays.stream(excludedClasses).collect(Collectors.toSet()));
 130  7
     }
 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  16
         excludeClassesRegexps.addAll(Arrays.stream(from.clone())
 138  8
                 .map(CommonUtils::createPattern)
 139  8
                 .collect(Collectors.toSet()));
 140  8
     }
 141  
 
 142  
     /**
 143  
      * Sets user-excluded pakcages to ignore. All exlcuded 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  12
         final List<String> invalidIdentifiers = Arrays.stream(excludedPackages)
 149  34
             .filter(x -> !CommonUtils.isName(x))
 150  12
             .collect(Collectors.toList());
 151  12
         if (!invalidIdentifiers.isEmpty()) {
 152  2
             throw new IllegalArgumentException(
 153  
                 "the following values are not valid identifiers: "
 154  2
                     + invalidIdentifiers.stream().collect(Collectors.joining(", ", "[", "]")));
 155  
         }
 156  
 
 157  20
         this.excludedPackages = Collections.unmodifiableSet(
 158  10
             Arrays.stream(excludedPackages).collect(Collectors.toSet()));
 159  10
     }
 160  
 
 161  
     @Override
 162  
     public final void beginTree(DetailAST ast) {
 163  18
         fileContext = new FileContext();
 164  18
     }
 165  
 
 166  
     @Override
 167  
     public void visitToken(DetailAST ast) {
 168  298
         switch (ast.getType()) {
 169  
             case TokenTypes.PACKAGE_DEF:
 170  18
                 visitPackageDef(ast);
 171  18
                 break;
 172  
             case TokenTypes.IMPORT:
 173  40
                 fileContext.registerImport(ast);
 174  40
                 break;
 175  
             case TokenTypes.CLASS_DEF:
 176  
             case TokenTypes.INTERFACE_DEF:
 177  
             case TokenTypes.ANNOTATION_DEF:
 178  
             case TokenTypes.ENUM_DEF:
 179  68
                 visitClassDef(ast);
 180  68
                 break;
 181  
             case TokenTypes.TYPE:
 182  71
                 fileContext.visitType(ast);
 183  71
                 break;
 184  
             case TokenTypes.LITERAL_NEW:
 185  92
                 fileContext.visitLiteralNew(ast);
 186  92
                 break;
 187  
             case TokenTypes.LITERAL_THROWS:
 188  8
                 fileContext.visitLiteralThrows(ast);
 189  8
                 break;
 190  
             default:
 191  1
                 throw new IllegalArgumentException("Unknown type: " + ast);
 192  
         }
 193  297
     }
 194  
 
 195  
     @Override
 196  
     public void leaveToken(DetailAST ast) {
 197  297
         switch (ast.getType()) {
 198  
             case TokenTypes.CLASS_DEF:
 199  
             case TokenTypes.INTERFACE_DEF:
 200  
             case TokenTypes.ANNOTATION_DEF:
 201  
             case TokenTypes.ENUM_DEF:
 202  68
                 leaveClassDef();
 203  68
                 break;
 204  
             default:
 205  
                 // Do nothing
 206  
         }
 207  297
     }
 208  
 
 209  
     /**
 210  
      * Stores package of current class we check.
 211  
      * @param pkg package definition.
 212  
      */
 213  
     private void visitPackageDef(DetailAST pkg) {
 214  18
         final FullIdent ident = FullIdent.createFullIdent(pkg.getLastChild().getPreviousSibling());
 215  18
         fileContext.setPackageName(ident.getText());
 216  18
     }
 217  
 
 218  
     /**
 219  
      * Creates new context for a given class.
 220  
      * @param classDef class definition node.
 221  
      */
 222  
     private void visitClassDef(DetailAST classDef) {
 223  68
         final String className = classDef.findFirstToken(TokenTypes.IDENT).getText();
 224  68
         fileContext.createNewClassContext(className, classDef.getLineNo(), classDef.getColumnNo());
 225  68
     }
 226  
 
 227  
     /** Restores previous context. */
 228  
     private void leaveClassDef() {
 229  68
         fileContext.checkCurrentClassAndRestorePrevious();
 230  68
     }
 231  
 
 232  
     /**
 233  
      * Encapsulates information about classes coupling inside single file.
 234  
      * @noinspection ThisEscapedInObjectConstruction
 235  
      */
 236  36
     private class FileContext {
 237  
         /** A map of (imported class name -> class name with package) pairs. */
 238  18
         private final Map<String, String> importedClassPackage = new HashMap<>();
 239  
 
 240  
         /** Stack of class contexts. */
 241  18
         private final Deque<ClassContext> classesContexts = new ArrayDeque<>();
 242  
 
 243  
         /** Current file package. */
 244  18
         private String packageName = "";
 245  
 
 246  
         /** Current context. */
 247  18
         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  68
             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  18
             this.packageName = packageName;
 263  18
         }
 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  80
             final FullIdent ident = FullIdent.createFullIdent(
 271  40
                 imp.getLastChild().getPreviousSibling());
 272  40
             final String fullName = ident.getText();
 273  40
             if (fullName.charAt(fullName.length() - 1) != '*') {
 274  28
                 final int lastDot = fullName.lastIndexOf(DOT);
 275  28
                 importedClassPackage.put(fullName.substring(lastDot + 1), fullName);
 276  
             }
 277  40
         }
 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  77
             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  68
             classesContexts.push(classContext);
 297  68
             classContext = new ClassContext(this, className, lineNo, columnNo);
 298  68
         }
 299  
 
 300  
         /** Restores previous context. */
 301  
         public void checkCurrentClassAndRestorePrevious() {
 302  68
             classContext.checkCoupling();
 303  68
             classContext = classesContexts.pop();
 304  68
         }
 305  
 
 306  
         /**
 307  
          * Visits type token for the current class context.
 308  
          * @param ast TYPE token.
 309  
          */
 310  
         public void visitType(DetailAST ast) {
 311  71
             classContext.visitType(ast);
 312  71
         }
 313  
 
 314  
         /**
 315  
          * Visits NEW token for the current class context.
 316  
          * @param ast NEW token.
 317  
          */
 318  
         public void visitLiteralNew(DetailAST ast) {
 319  92
             classContext.visitLiteralNew(ast);
 320  92
         }
 321  
 
 322  
         /**
 323  
          * Visits THROWS token for the current class context.
 324  
          * @param ast THROWS token.
 325  
          */
 326  
         public void visitLiteralThrows(DetailAST ast) {
 327  8
             classContext.visitLiteralThrows(ast);
 328  8
         }
 329  
     }
 330  
 
 331  
     /**
 332  
      * Encapsulates information about class coupling.
 333  
      *
 334  
      * @author <a href="mailto:simon@redhillconsulting.com.au">Simon Harris</a>
 335  
      * @author o_sukhodolsky
 336  
      */
 337  
     private class ClassContext {
 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  86
         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  86
         ClassContext(FileContext parentContext, String className, int lineNo, int columnNo) {
 361  86
             this.parentContext = parentContext;
 362  86
             this.className = className;
 363  86
             this.lineNo = lineNo;
 364  86
             this.columnNo = columnNo;
 365  86
         }
 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  8
             for (DetailAST childAST = literalThrows.getFirstChild();
 373  24
                  childAST != null;
 374  16
                  childAST = childAST.getNextSibling()) {
 375  16
                 if (childAST.getType() != TokenTypes.COMMA) {
 376  12
                     addReferencedClassName(childAST);
 377  
                 }
 378  
             }
 379  8
         }
 380  
 
 381  
         /**
 382  
          * Visits type.
 383  
          * @param ast type to process.
 384  
          */
 385  
         public void visitType(DetailAST ast) {
 386  71
             final String fullTypeName = CheckUtils.createFullType(ast).getText();
 387  71
             addReferencedClassName(fullTypeName);
 388  71
         }
 389  
 
 390  
         /**
 391  
          * Visits NEW.
 392  
          * @param ast NEW to process.
 393  
          */
 394  
         public void visitLiteralNew(DetailAST ast) {
 395  92
             addReferencedClassName(ast.getFirstChild());
 396  92
         }
 397  
 
 398  
         /**
 399  
          * Adds new referenced class.
 400  
          * @param ast a node which represents referenced class.
 401  
          */
 402  
         private void addReferencedClassName(DetailAST ast) {
 403  104
             final String fullIdentName = FullIdent.createFullIdent(ast).getText();
 404  104
             addReferencedClassName(fullIdentName);
 405  104
         }
 406  
 
 407  
         /**
 408  
          * Adds new referenced class.
 409  
          * @param referencedClassName class name of the referenced class.
 410  
          */
 411  
         private void addReferencedClassName(String referencedClassName) {
 412  175
             if (isSignificant(referencedClassName)) {
 413  55
                 referencedClassNames.add(referencedClassName);
 414  
             }
 415  175
         }
 416  
 
 417  
         /** Checks if coupling less than allowed or not. */
 418  
         public void checkCoupling() {
 419  68
             referencedClassNames.remove(className);
 420  68
             referencedClassNames.remove(parentContext.getPackageName() + DOT + className);
 421  
 
 422  68
             if (referencedClassNames.size() > max) {
 423  44
                 log(lineNo, columnNo, getLogMessageId(),
 424  22
                         referencedClassNames.size(), max,
 425  22
                         referencedClassNames.toString());
 426  
             }
 427  68
         }
 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  175
             boolean result = !excludedClasses.contains(candidateClassName)
 436  85
                 && !isFromExcludedPackage(candidateClassName);
 437  175
             if (result) {
 438  61
                 for (Pattern pattern : excludeClassesRegexps) {
 439  74
                     if (pattern.matcher(candidateClassName).matches()) {
 440  6
                         result = false;
 441  6
                         break;
 442  
                     }
 443  68
                 }
 444  
             }
 445  175
             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  85
             String classNameWithPackage = candidateClassName;
 455  85
             if (!candidateClassName.contains(DOT)) {
 456  77
                 classNameWithPackage = parentContext.getClassNameWithPackage(candidateClassName)
 457  77
                     .orElse("");
 458  
             }
 459  85
             boolean isFromExcludedPackage = false;
 460  85
             if (classNameWithPackage.contains(DOT)) {
 461  38
                 final int lastDotIndex = classNameWithPackage.lastIndexOf(DOT);
 462  38
                 final String packageName = classNameWithPackage.substring(0, lastDotIndex);
 463  38
                 isFromExcludedPackage = packageName.startsWith("java.lang")
 464  30
                     || excludedPackages.contains(packageName);
 465  
             }
 466  85
             return isFromExcludedPackage;
 467  
         }
 468  
     }
 469  
 }