1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 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.CheckUtil;
42  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
43  
44  /**
45   * Base class for coupling calculation.
46   *
47   */
48  @FileStatefulCheck
49  public abstract class AbstractClassCouplingCheck extends AbstractCheck {
50  
51      /** A package separator - "." */
52      private static final String DOT = ".";
53  
54      /** Class names to ignore. */
55      private static final Set<String> DEFAULT_EXCLUDED_CLASSES = Collections.unmodifiableSet(
56          Arrays.stream(new String[] {
57              // primitives
58              "boolean", "byte", "char", "double", "float", "int",
59              "long", "short", "void",
60              // wrappers
61              "Boolean", "Byte", "Character", "Double", "Float",
62              "Integer", "Long", "Short", "Void",
63              // java.lang.*
64              "Object", "Class",
65              "String", "StringBuffer", "StringBuilder",
66              // Exceptions
67              "ArrayIndexOutOfBoundsException", "Exception",
68              "RuntimeException", "IllegalArgumentException",
69              "IllegalStateException", "IndexOutOfBoundsException",
70              "NullPointerException", "Throwable", "SecurityException",
71              "UnsupportedOperationException",
72              // java.util.*
73              "List", "ArrayList", "Deque", "Queue", "LinkedList",
74              "Set", "HashSet", "SortedSet", "TreeSet",
75              "Map", "HashMap", "SortedMap", "TreeMap",
76              "Override", "Deprecated", "SafeVarargs", "SuppressWarnings", "FunctionalInterface",
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      /** A map of (imported class name -> class name with package) pairs. */
86      private final Map<String, String> importedClassPackages = new HashMap<>();
87  
88      /** Stack of class contexts. */
89      private final Deque<ClassContext> classesContexts = new ArrayDeque<>();
90  
91      /** User-configured class names to ignore. */
92      private Set<String> excludedClasses = DEFAULT_EXCLUDED_CLASSES;
93      /** User-configured package names to ignore. */
94      private Set<String> excludedPackages = DEFAULT_EXCLUDED_PACKAGES;
95      /** Allowed complexity. */
96      private int max;
97  
98      /** Current file package. */
99      private String packageName;
100 
101     /**
102      * Creates new instance of the check.
103      * @param defaultMax default value for allowed complexity.
104      */
105     protected AbstractClassCouplingCheck(int defaultMax) {
106         max = defaultMax;
107         excludeClassesRegexps.add(CommonUtil.createPattern("^$"));
108     }
109 
110     /**
111      * Returns message key we use for log violations.
112      * @return message key we use for log violations.
113      */
114     protected abstract String getLogMessageId();
115 
116     @Override
117     public final int[] getDefaultTokens() {
118         return getRequiredTokens();
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.addAll(Arrays.stream(from.clone())
144                 .map(CommonUtil::createPattern)
145                 .collect(Collectors.toSet()));
146     }
147 
148     /**
149      * Sets user-excluded packages to ignore. All excluded packages should end with a period,
150      * so it also appends a dot to a package name.
151      * @param excludedPackages the list of packages to ignore.
152      */
153     public final void setExcludedPackages(String... excludedPackages) {
154         final List<String> invalidIdentifiers = Arrays.stream(excludedPackages)
155             .filter(excludedPackageName -> !CommonUtil.isName(excludedPackageName))
156             .collect(Collectors.toList());
157         if (!invalidIdentifiers.isEmpty()) {
158             throw new IllegalArgumentException(
159                 "the following values are not valid identifiers: "
160                     + invalidIdentifiers.stream().collect(Collectors.joining(", ", "[", "]")));
161         }
162 
163         this.excludedPackages = Collections.unmodifiableSet(
164             Arrays.stream(excludedPackages).collect(Collectors.toSet()));
165     }
166 
167     @Override
168     public final void beginTree(DetailAST ast) {
169         importedClassPackages.clear();
170         classesContexts.clear();
171         classesContexts.push(new ClassContext("", null));
172         packageName = "";
173     }
174 
175     @Override
176     public void visitToken(DetailAST ast) {
177         switch (ast.getType()) {
178             case TokenTypes.PACKAGE_DEF:
179                 visitPackageDef(ast);
180                 break;
181             case TokenTypes.IMPORT:
182                 registerImport(ast);
183                 break;
184             case TokenTypes.CLASS_DEF:
185             case TokenTypes.INTERFACE_DEF:
186             case TokenTypes.ANNOTATION_DEF:
187             case TokenTypes.ENUM_DEF:
188                 visitClassDef(ast);
189                 break;
190             case TokenTypes.EXTENDS_CLAUSE:
191             case TokenTypes.IMPLEMENTS_CLAUSE:
192             case TokenTypes.TYPE:
193                 visitType(ast);
194                 break;
195             case TokenTypes.LITERAL_NEW:
196                 visitLiteralNew(ast);
197                 break;
198             case TokenTypes.LITERAL_THROWS:
199                 visitLiteralThrows(ast);
200                 break;
201             case TokenTypes.ANNOTATION:
202                 visitAnnotationType(ast);
203                 break;
204             default:
205                 throw new IllegalArgumentException("Unknown type: " + ast);
206         }
207     }
208 
209     @Override
210     public void leaveToken(DetailAST ast) {
211         switch (ast.getType()) {
212             case TokenTypes.CLASS_DEF:
213             case TokenTypes.INTERFACE_DEF:
214             case TokenTypes.ANNOTATION_DEF:
215             case TokenTypes.ENUM_DEF:
216                 leaveClassDef();
217                 break;
218             default:
219                 // Do nothing
220         }
221     }
222 
223     /**
224      * Stores package of current class we check.
225      * @param pkg package definition.
226      */
227     private void visitPackageDef(DetailAST pkg) {
228         final FullIdent ident = FullIdent.createFullIdent(pkg.getLastChild().getPreviousSibling());
229         packageName = ident.getText();
230     }
231 
232     /**
233      * Creates new context for a given class.
234      * @param classDef class definition node.
235      */
236     private void visitClassDef(DetailAST classDef) {
237         final String className = classDef.findFirstToken(TokenTypes.IDENT).getText();
238         createNewClassContext(className, classDef);
239     }
240 
241     /** Restores previous context. */
242     private void leaveClassDef() {
243         checkCurrentClassAndRestorePrevious();
244     }
245 
246     /**
247      * Registers given import. This allows us to track imported classes.
248      * @param imp import definition.
249      */
250     private void registerImport(DetailAST imp) {
251         final FullIdent ident = FullIdent.createFullIdent(
252             imp.getLastChild().getPreviousSibling());
253         final String fullName = ident.getText();
254         final int lastDot = fullName.lastIndexOf(DOT);
255         importedClassPackages.put(fullName.substring(lastDot + 1), fullName);
256     }
257 
258     /**
259      * Creates new inner class context with given name and location.
260      * @param className The class name.
261      * @param ast The class ast.
262      */
263     private void createNewClassContext(String className, DetailAST ast) {
264         classesContexts.push(new ClassContext(className, ast));
265     }
266 
267     /** Restores previous context. */
268     private void checkCurrentClassAndRestorePrevious() {
269         classesContexts.pop().checkCoupling();
270     }
271 
272     /**
273      * Visits type token for the current class context.
274      * @param ast TYPE token.
275      */
276     private void visitType(DetailAST ast) {
277         classesContexts.peek().visitType(ast);
278     }
279 
280     /**
281      * Visits NEW token for the current class context.
282      * @param ast NEW token.
283      */
284     private void visitLiteralNew(DetailAST ast) {
285         classesContexts.peek().visitLiteralNew(ast);
286     }
287 
288     /**
289      * Visits THROWS token for the current class context.
290      * @param ast THROWS token.
291      */
292     private void visitLiteralThrows(DetailAST ast) {
293         classesContexts.peek().visitLiteralThrows(ast);
294     }
295 
296     /**
297      * Visit ANNOTATION literal and get its type to referenced classes of context.
298      * @param annotationAST Annotation ast.
299      */
300     private void visitAnnotationType(DetailAST annotationAST) {
301         final DetailAST children = annotationAST.getFirstChild();
302         final DetailAST type = children.getNextSibling();
303         classesContexts.peek().addReferencedClassName(type.getText());
304     }
305 
306     /**
307      * Encapsulates information about class coupling.
308      *
309      */
310     private class ClassContext {
311 
312         /**
313          * Set of referenced classes.
314          * Sorted by name for predictable error messages in unit tests.
315          */
316         private final Set<String> referencedClassNames = new TreeSet<>();
317         /** Own class name. */
318         private final String className;
319         /* Location of own class. (Used to log violations) */
320         /** AST of class definition. */
321         private final DetailAST classAst;
322 
323         /**
324          * Create new context associated with given class.
325          * @param className name of the given class.
326          * @param ast ast of class definition.
327          */
328         /* package */ ClassContext(String className, DetailAST ast) {
329             this.className = className;
330             classAst = ast;
331         }
332 
333         /**
334          * Visits throws clause and collects all exceptions we throw.
335          * @param literalThrows throws to process.
336          */
337         public void visitLiteralThrows(DetailAST literalThrows) {
338             for (DetailAST childAST = literalThrows.getFirstChild();
339                  childAST != null;
340                  childAST = childAST.getNextSibling()) {
341                 if (childAST.getType() != TokenTypes.COMMA) {
342                     addReferencedClassName(childAST);
343                 }
344             }
345         }
346 
347         /**
348          * Visits type.
349          * @param ast type to process.
350          */
351         public void visitType(DetailAST ast) {
352             final String fullTypeName = CheckUtil.createFullType(ast).getText();
353             addReferencedClassName(fullTypeName);
354         }
355 
356         /**
357          * Visits NEW.
358          * @param ast NEW to process.
359          */
360         public void visitLiteralNew(DetailAST ast) {
361             addReferencedClassName(ast.getFirstChild());
362         }
363 
364         /**
365          * Adds new referenced class.
366          * @param ast a node which represents referenced class.
367          */
368         private void addReferencedClassName(DetailAST ast) {
369             final String fullIdentName = FullIdent.createFullIdent(ast).getText();
370             addReferencedClassName(fullIdentName);
371         }
372 
373         /**
374          * Adds new referenced class.
375          * @param referencedClassName class name of the referenced class.
376          */
377         private void addReferencedClassName(String referencedClassName) {
378             if (isSignificant(referencedClassName)) {
379                 referencedClassNames.add(referencedClassName);
380             }
381         }
382 
383         /** Checks if coupling less than allowed or not. */
384         public void checkCoupling() {
385             referencedClassNames.remove(className);
386             referencedClassNames.remove(packageName + DOT + className);
387 
388             if (referencedClassNames.size() > max) {
389                 log(classAst, getLogMessageId(),
390                         referencedClassNames.size(), max,
391                         referencedClassNames.toString());
392             }
393         }
394 
395         /**
396          * Checks if given class shouldn't be ignored and not from java.lang.
397          * @param candidateClassName class to check.
398          * @return true if we should count this class.
399          */
400         private boolean isSignificant(String candidateClassName) {
401             return !excludedClasses.contains(candidateClassName)
402                 && !isFromExcludedPackage(candidateClassName)
403                 && !isExcludedClassRegexp(candidateClassName);
404         }
405 
406         /**
407          * Checks if given class should be ignored as it belongs to excluded package.
408          * @param candidateClassName class to check
409          * @return true if we should not count this class.
410          */
411         private boolean isFromExcludedPackage(String candidateClassName) {
412             String classNameWithPackage = candidateClassName;
413             if (!candidateClassName.contains(DOT)) {
414                 classNameWithPackage = getClassNameWithPackage(candidateClassName)
415                     .orElse("");
416             }
417             boolean isFromExcludedPackage = false;
418             if (classNameWithPackage.contains(DOT)) {
419                 final int lastDotIndex = classNameWithPackage.lastIndexOf(DOT);
420                 final String candidatePackageName =
421                     classNameWithPackage.substring(0, lastDotIndex);
422                 isFromExcludedPackage = candidatePackageName.startsWith("java.lang")
423                     || excludedPackages.contains(candidatePackageName);
424             }
425             return isFromExcludedPackage;
426         }
427 
428         /**
429          * Retrieves class name with packages. Uses previously registered imports to
430          * get the full class name.
431          * @param examineClassName Class name to be retrieved.
432          * @return Class name with package name, if found, {@link Optional#empty()} otherwise.
433          */
434         private Optional<String> getClassNameWithPackage(String examineClassName) {
435             return Optional.ofNullable(importedClassPackages.get(examineClassName));
436         }
437 
438         /**
439          * Checks if given class should be ignored as it belongs to excluded class regexp.
440          * @param candidateClassName class to check.
441          * @return true if we should not count this class.
442          */
443         private boolean isExcludedClassRegexp(String candidateClassName) {
444             boolean result = false;
445             for (Pattern pattern : excludeClassesRegexps) {
446                 if (pattern.matcher(candidateClassName).matches()) {
447                     result = true;
448                     break;
449                 }
450             }
451             return result;
452         }
453 
454     }
455 
456 }