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