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