View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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.function.Predicate;
34  import java.util.regex.Pattern;
35  import java.util.stream.Collectors;
36  
37  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
38  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
39  import com.puppycrawl.tools.checkstyle.api.DetailAST;
40  import com.puppycrawl.tools.checkstyle.api.FullIdent;
41  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
42  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
43  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
44  
45  /**
46   * Base class for coupling calculation.
47   *
48   */
49  @FileStatefulCheck
50  public abstract class AbstractClassCouplingCheck extends AbstractCheck {
51  
52      /** A package separator - "." */
53      private static final char DOT = '.';
54  
55      /** Class names to ignore. */
56      private static final Set<String> DEFAULT_EXCLUDED_CLASSES = Set.of(
57          // reserved type name
58          "var",
59          // primitives
60          "boolean", "byte", "char", "double", "float", "int",
61          "long", "short", "void",
62          // wrappers
63          "Boolean", "Byte", "Character", "Double", "Float",
64          "Integer", "Long", "Short", "Void",
65          // java.lang.*
66          "Object", "Class",
67          "String", "StringBuffer", "StringBuilder",
68          // Exceptions
69          "ArrayIndexOutOfBoundsException", "Exception",
70          "RuntimeException", "IllegalArgumentException",
71          "IllegalStateException", "IndexOutOfBoundsException",
72          "NullPointerException", "Throwable", "SecurityException",
73          "UnsupportedOperationException",
74          // java.util.*
75          "List", "ArrayList", "Deque", "Queue", "LinkedList",
76          "Set", "HashSet", "SortedSet", "TreeSet",
77          "Map", "HashMap", "SortedMap", "TreeMap",
78          "Override", "Deprecated", "SafeVarargs", "SuppressWarnings", "FunctionalInterface",
79          "Collection", "EnumSet", "LinkedHashMap", "LinkedHashSet", "Optional",
80          "OptionalDouble", "OptionalInt", "OptionalLong",
81          // java.util.stream.*
82          "DoubleStream", "IntStream", "LongStream", "Stream"
83      );
84  
85      /** Package names to ignore. */
86      private static final Set<String> DEFAULT_EXCLUDED_PACKAGES = Collections.emptySet();
87  
88      /** Pattern to match brackets in a full type name. */
89      private static final Pattern BRACKET_PATTERN = Pattern.compile("\\[[^]]*]");
90  
91      /** Specify user-configured regular expressions to ignore classes. */
92      private final List<Pattern> excludeClassesRegexps = new ArrayList<>();
93  
94      /** A map of (imported class name -&gt; class name with package) pairs. */
95      private final Map<String, String> importedClassPackages = new HashMap<>();
96  
97      /** Stack of class contexts. */
98      private final Deque<ClassContext> classesContexts = new ArrayDeque<>();
99  
100     /** Specify user-configured class names to ignore. */
101     private Set<String> excludedClasses = DEFAULT_EXCLUDED_CLASSES;
102 
103     /**
104      * Specify user-configured packages to ignore.
105      */
106     private Set<String> excludedPackages = DEFAULT_EXCLUDED_PACKAGES;
107 
108     /** Specify the maximum threshold allowed. */
109     private int max;
110 
111     /** Current file package. */
112     private String packageName;
113 
114     /**
115      * Creates new instance of the check.
116      *
117      * @param defaultMax default value for allowed complexity.
118      */
119     protected AbstractClassCouplingCheck(int defaultMax) {
120         max = defaultMax;
121         excludeClassesRegexps.add(CommonUtil.createPattern("^$"));
122     }
123 
124     /**
125      * Returns message key we use for log violations.
126      *
127      * @return message key we use for log violations.
128      */
129     protected abstract String getLogMessageId();
130 
131     @Override
132     public final int[] getDefaultTokens() {
133         return getRequiredTokens();
134     }
135 
136     /**
137      * Setter to specify the maximum threshold allowed.
138      *
139      * @param max allowed complexity.
140      */
141     public final void setMax(int max) {
142         this.max = max;
143     }
144 
145     /**
146      * Setter to specify user-configured class names to ignore.
147      *
148      * @param excludedClasses classes to ignore.
149      */
150     public final void setExcludedClasses(String... excludedClasses) {
151         this.excludedClasses = Set.of(excludedClasses);
152     }
153 
154     /**
155      * Setter to specify user-configured regular expressions to ignore classes.
156      *
157      * @param from array representing regular expressions of classes to ignore.
158      */
159     public void setExcludeClassesRegexps(String... from) {
160         Arrays.stream(from)
161                 .map(CommonUtil::createPattern)
162                 .forEach(excludeClassesRegexps::add);
163     }
164 
165     /**
166      * Setter to specify user-configured packages to ignore.
167      *
168      * @param excludedPackages packages to ignore.
169      * @throws IllegalArgumentException if there are invalid identifiers among the packages.
170      */
171     public final void setExcludedPackages(String... excludedPackages) {
172         final List<String> invalidIdentifiers = Arrays.stream(excludedPackages)
173             .filter(Predicate.not(CommonUtil::isName))
174             .collect(Collectors.toUnmodifiableList());
175         if (!invalidIdentifiers.isEmpty()) {
176             throw new IllegalArgumentException(
177                 "the following values are not valid identifiers: " + invalidIdentifiers);
178         }
179 
180         this.excludedPackages = Set.of(excludedPackages);
181     }
182 
183     @Override
184     public final void beginTree(DetailAST ast) {
185         importedClassPackages.clear();
186         classesContexts.clear();
187         classesContexts.push(new ClassContext("", null));
188         packageName = "";
189     }
190 
191     @Override
192     public void visitToken(DetailAST ast) {
193         switch (ast.getType()) {
194             case TokenTypes.PACKAGE_DEF:
195                 visitPackageDef(ast);
196                 break;
197             case TokenTypes.IMPORT:
198                 registerImport(ast);
199                 break;
200             case TokenTypes.CLASS_DEF:
201             case TokenTypes.INTERFACE_DEF:
202             case TokenTypes.ANNOTATION_DEF:
203             case TokenTypes.ENUM_DEF:
204             case TokenTypes.RECORD_DEF:
205                 visitClassDef(ast);
206                 break;
207             case TokenTypes.EXTENDS_CLAUSE:
208             case TokenTypes.IMPLEMENTS_CLAUSE:
209             case TokenTypes.TYPE:
210                 visitType(ast);
211                 break;
212             case TokenTypes.LITERAL_NEW:
213                 visitLiteralNew(ast);
214                 break;
215             case TokenTypes.LITERAL_THROWS:
216                 visitLiteralThrows(ast);
217                 break;
218             case TokenTypes.ANNOTATION:
219                 visitAnnotationType(ast);
220                 break;
221             default:
222                 throw new IllegalArgumentException("Unknown type: " + ast);
223         }
224     }
225 
226     @Override
227     public void leaveToken(DetailAST ast) {
228         if (TokenUtil.isTypeDeclaration(ast.getType())) {
229             leaveClassDef();
230         }
231     }
232 
233     /**
234      * Stores package of current class we check.
235      *
236      * @param pkg package definition.
237      */
238     private void visitPackageDef(DetailAST pkg) {
239         final FullIdent ident = FullIdent.createFullIdent(pkg.getLastChild().getPreviousSibling());
240         packageName = ident.getText();
241     }
242 
243     /**
244      * Creates new context for a given class.
245      *
246      * @param classDef class definition node.
247      */
248     private void visitClassDef(DetailAST classDef) {
249         final String className = classDef.findFirstToken(TokenTypes.IDENT).getText();
250         createNewClassContext(className, classDef);
251     }
252 
253     /** Restores previous context. */
254     private void leaveClassDef() {
255         checkCurrentClassAndRestorePrevious();
256     }
257 
258     /**
259      * Registers given import. This allows us to track imported classes.
260      *
261      * @param imp import definition.
262      */
263     private void registerImport(DetailAST imp) {
264         final FullIdent ident = FullIdent.createFullIdent(
265             imp.getLastChild().getPreviousSibling());
266         final String fullName = ident.getText();
267         final int lastDot = fullName.lastIndexOf(DOT);
268         importedClassPackages.put(fullName.substring(lastDot + 1), fullName);
269     }
270 
271     /**
272      * Creates new inner class context with given name and location.
273      *
274      * @param className The class name.
275      * @param ast The class ast.
276      */
277     private void createNewClassContext(String className, DetailAST ast) {
278         classesContexts.push(new ClassContext(className, ast));
279     }
280 
281     /** Restores previous context. */
282     private void checkCurrentClassAndRestorePrevious() {
283         classesContexts.pop().checkCoupling();
284     }
285 
286     /**
287      * Visits type token for the current class context.
288      *
289      * @param ast TYPE token.
290      */
291     private void visitType(DetailAST ast) {
292         classesContexts.peek().visitType(ast);
293     }
294 
295     /**
296      * Visits NEW token for the current class context.
297      *
298      * @param ast NEW token.
299      */
300     private void visitLiteralNew(DetailAST ast) {
301         classesContexts.peek().visitLiteralNew(ast);
302     }
303 
304     /**
305      * Visits THROWS token for the current class context.
306      *
307      * @param ast THROWS token.
308      */
309     private void visitLiteralThrows(DetailAST ast) {
310         classesContexts.peek().visitLiteralThrows(ast);
311     }
312 
313     /**
314      * Visit ANNOTATION literal and get its type to referenced classes of context.
315      *
316      * @param annotationAST Annotation ast.
317      */
318     private void visitAnnotationType(DetailAST annotationAST) {
319         final DetailAST children = annotationAST.getFirstChild();
320         final DetailAST type = children.getNextSibling();
321         classesContexts.peek().addReferencedClassName(type.getText());
322     }
323 
324     /**
325      * Encapsulates information about class coupling.
326      *
327      */
328     private final class ClassContext {
329 
330         /**
331          * Set of referenced classes.
332          * Sorted by name for predictable violation messages in unit tests.
333          */
334         private final Set<String> referencedClassNames = new TreeSet<>();
335         /** Own class name. */
336         private final String className;
337         /* Location of own class. (Used to log violations) */
338         /** AST of class definition. */
339         private final DetailAST classAst;
340 
341         /**
342          * Create new context associated with given class.
343          *
344          * @param className name of the given class.
345          * @param ast ast of class definition.
346          */
347         private ClassContext(String className, DetailAST ast) {
348             this.className = className;
349             classAst = ast;
350         }
351 
352         /**
353          * Visits throws clause and collects all exceptions we throw.
354          *
355          * @param literalThrows throws to process.
356          */
357         public void visitLiteralThrows(DetailAST literalThrows) {
358             for (DetailAST childAST = literalThrows.getFirstChild();
359                  childAST != null;
360                  childAST = childAST.getNextSibling()) {
361                 if (childAST.getType() != TokenTypes.COMMA) {
362                     addReferencedClassName(childAST);
363                 }
364             }
365         }
366 
367         /**
368          * Visits type.
369          *
370          * @param ast type to process.
371          */
372         public void visitType(DetailAST ast) {
373             DetailAST child = ast.getFirstChild();
374             while (child != null) {
375                 if (TokenUtil.isOfType(child, TokenTypes.IDENT, TokenTypes.DOT)) {
376                     final String fullTypeName = FullIdent.createFullIdent(child).getText();
377                     final String trimmed = BRACKET_PATTERN
378                             .matcher(fullTypeName).replaceAll("");
379                     addReferencedClassName(trimmed);
380                 }
381                 child = child.getNextSibling();
382             }
383         }
384 
385         /**
386          * Visits NEW.
387          *
388          * @param ast NEW to process.
389          */
390         public void visitLiteralNew(DetailAST ast) {
391             addReferencedClassName(ast.getFirstChild());
392         }
393 
394         /**
395          * Adds new referenced class.
396          *
397          * @param ast a node which represents referenced class.
398          */
399         private void addReferencedClassName(DetailAST ast) {
400             final String fullIdentName = FullIdent.createFullIdent(ast).getText();
401             final String trimmed = BRACKET_PATTERN
402                     .matcher(fullIdentName).replaceAll("");
403             addReferencedClassName(trimmed);
404         }
405 
406         /**
407          * Adds new referenced class.
408          *
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(packageName + DOT + className);
421 
422             if (referencedClassNames.size() > max) {
423                 log(classAst, 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          *
432          * @param candidateClassName class to check.
433          * @return true if we should count this class.
434          */
435         private boolean isSignificant(String candidateClassName) {
436             return !excludedClasses.contains(candidateClassName)
437                 && !isFromExcludedPackage(candidateClassName)
438                 && !isExcludedClassRegexp(candidateClassName);
439         }
440 
441         /**
442          * Checks if given class should be ignored as it belongs to excluded package.
443          *
444          * @param candidateClassName class to check
445          * @return true if we should not count this class.
446          */
447         private boolean isFromExcludedPackage(String candidateClassName) {
448             String classNameWithPackage = candidateClassName;
449             if (candidateClassName.indexOf(DOT) == -1) {
450                 classNameWithPackage = getClassNameWithPackage(candidateClassName)
451                     .orElse("");
452             }
453             boolean isFromExcludedPackage = false;
454             if (classNameWithPackage.indexOf(DOT) != -1) {
455                 final int lastDotIndex = classNameWithPackage.lastIndexOf(DOT);
456                 final String candidatePackageName =
457                     classNameWithPackage.substring(0, lastDotIndex);
458                 isFromExcludedPackage = candidatePackageName.startsWith("java.lang")
459                     || excludedPackages.contains(candidatePackageName);
460             }
461             return isFromExcludedPackage;
462         }
463 
464         /**
465          * Retrieves class name with packages. Uses previously registered imports to
466          * get the full class name.
467          *
468          * @param examineClassName Class name to be retrieved.
469          * @return Class name with package name, if found, {@link Optional#empty()} otherwise.
470          */
471         private Optional<String> getClassNameWithPackage(String examineClassName) {
472             return Optional.ofNullable(importedClassPackages.get(examineClassName));
473         }
474 
475         /**
476          * Checks if given class should be ignored as it belongs to excluded class regexp.
477          *
478          * @param candidateClassName class to check.
479          * @return true if we should not count this class.
480          */
481         private boolean isExcludedClassRegexp(String candidateClassName) {
482             boolean result = false;
483             for (Pattern pattern : excludeClassesRegexps) {
484                 if (pattern.matcher(candidateClassName).matches()) {
485                     result = true;
486                     break;
487                 }
488             }
489             return result;
490         }
491 
492     }
493 
494 }