View Javadoc
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  public abstract class AbstractClassCouplingCheck extends AbstractCheck {
52      /** A package separator - "." */
53      private static final String DOT = ".";
54  
55      /** Class names to ignore. */
56      private static final Set<String> DEFAULT_EXCLUDED_CLASSES = Collections.unmodifiableSet(
57          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          }).collect(Collectors.toSet()));
78  
79      /** Package names to ignore. */
80      private static final Set<String> DEFAULT_EXCLUDED_PACKAGES = Collections.emptySet();
81  
82      /** User-configured regular expressions to ignore classes. */
83      private final List<Pattern> excludeClassesRegexps = new ArrayList<>();
84  
85      /** User-configured class names to ignore. */
86      private Set<String> excludedClasses = DEFAULT_EXCLUDED_CLASSES;
87      /** User-configured package names to ignore. */
88      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      protected AbstractClassCouplingCheck(int defaultMax) {
100         max = defaultMax;
101         excludeClassesRegexps.add(CommonUtils.createPattern("^$"));
102     }
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         return getRequiredTokens();
113     }
114 
115     /**
116      * Sets maximum allowed complexity.
117      * @param max allowed complexity.
118      */
119     public final void setMax(int max) {
120         this.max = max;
121     }
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         this.excludedClasses =
129             Collections.unmodifiableSet(Arrays.stream(excludedClasses).collect(Collectors.toSet()));
130     }
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         excludeClassesRegexps.addAll(Arrays.stream(from.clone())
138                 .map(CommonUtils::createPattern)
139                 .collect(Collectors.toSet()));
140     }
141 
142     /**
143      * Sets user-excluded packages to ignore. All excluded 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         final List<String> invalidIdentifiers = Arrays.stream(excludedPackages)
149             .filter(x -> !CommonUtils.isName(x))
150             .collect(Collectors.toList());
151         if (!invalidIdentifiers.isEmpty()) {
152             throw new IllegalArgumentException(
153                 "the following values are not valid identifiers: "
154                     + invalidIdentifiers.stream().collect(Collectors.joining(", ", "[", "]")));
155         }
156 
157         this.excludedPackages = Collections.unmodifiableSet(
158             Arrays.stream(excludedPackages).collect(Collectors.toSet()));
159     }
160 
161     @Override
162     public final void beginTree(DetailAST ast) {
163         fileContext = new FileContext();
164     }
165 
166     @Override
167     public void visitToken(DetailAST ast) {
168         switch (ast.getType()) {
169             case TokenTypes.PACKAGE_DEF:
170                 visitPackageDef(ast);
171                 break;
172             case TokenTypes.IMPORT:
173                 fileContext.registerImport(ast);
174                 break;
175             case TokenTypes.CLASS_DEF:
176             case TokenTypes.INTERFACE_DEF:
177             case TokenTypes.ANNOTATION_DEF:
178             case TokenTypes.ENUM_DEF:
179                 visitClassDef(ast);
180                 break;
181             case TokenTypes.TYPE:
182                 fileContext.visitType(ast);
183                 break;
184             case TokenTypes.LITERAL_NEW:
185                 fileContext.visitLiteralNew(ast);
186                 break;
187             case TokenTypes.LITERAL_THROWS:
188                 fileContext.visitLiteralThrows(ast);
189                 break;
190             default:
191                 throw new IllegalArgumentException("Unknown type: " + ast);
192         }
193     }
194 
195     @Override
196     public void leaveToken(DetailAST ast) {
197         switch (ast.getType()) {
198             case TokenTypes.CLASS_DEF:
199             case TokenTypes.INTERFACE_DEF:
200             case TokenTypes.ANNOTATION_DEF:
201             case TokenTypes.ENUM_DEF:
202                 leaveClassDef();
203                 break;
204             default:
205                 // Do nothing
206         }
207     }
208 
209     /**
210      * Stores package of current class we check.
211      * @param pkg package definition.
212      */
213     private void visitPackageDef(DetailAST pkg) {
214         final FullIdent ident = FullIdent.createFullIdent(pkg.getLastChild().getPreviousSibling());
215         fileContext.setPackageName(ident.getText());
216     }
217 
218     /**
219      * Creates new context for a given class.
220      * @param classDef class definition node.
221      */
222     private void visitClassDef(DetailAST classDef) {
223         final String className = classDef.findFirstToken(TokenTypes.IDENT).getText();
224         fileContext.createNewClassContext(className, classDef.getLineNo(), classDef.getColumnNo());
225     }
226 
227     /** Restores previous context. */
228     private void leaveClassDef() {
229         fileContext.checkCurrentClassAndRestorePrevious();
230     }
231 
232     /**
233      * Encapsulates information about classes coupling inside single file.
234      * @noinspection ThisEscapedInObjectConstruction
235      */
236     private class FileContext {
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      * 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         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 = CheckUtils.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 }