001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.metrics;
021
022import java.util.ArrayDeque;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Collections;
026import java.util.Deque;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Map;
030import java.util.Optional;
031import java.util.Set;
032import java.util.TreeSet;
033import java.util.function.Predicate;
034import java.util.regex.Pattern;
035
036import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
037import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
038import com.puppycrawl.tools.checkstyle.api.DetailAST;
039import com.puppycrawl.tools.checkstyle.api.FullIdent;
040import com.puppycrawl.tools.checkstyle.api.TokenTypes;
041import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
042import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
043
044/**
045 * Base class for coupling calculation.
046 *
047 */
048@FileStatefulCheck
049public abstract class AbstractClassCouplingCheck extends AbstractCheck {
050
051    /** A package separator - ".". */
052    private static final char DOT = '.';
053
054    /** Class names to ignore. */
055    private static final Set<String> DEFAULT_EXCLUDED_CLASSES = Set.of(
056        // reserved type name
057        "var",
058        // primitives
059        "boolean", "byte", "char", "double", "float", "int",
060        "long", "short", "void",
061        // wrappers
062        "Boolean", "Byte", "Character", "Double", "Float",
063        "Integer", "Long", "Short", "Void",
064        // java.lang.*
065        "Object", "Class",
066        "String", "StringBuffer", "StringBuilder",
067        // Exceptions
068        "ArrayIndexOutOfBoundsException", "Exception",
069        "RuntimeException", "IllegalArgumentException",
070        "IllegalStateException", "IndexOutOfBoundsException",
071        "NullPointerException", "Throwable", "SecurityException",
072        "UnsupportedOperationException",
073        // java.util.*
074        "List", "ArrayList", "Deque", "Queue", "LinkedList",
075        "Set", "HashSet", "SortedSet", "TreeSet",
076        "Map", "HashMap", "SortedMap", "TreeMap",
077        "Override", "Deprecated", "SafeVarargs", "SuppressWarnings", "FunctionalInterface",
078        "Collection", "EnumSet", "LinkedHashMap", "LinkedHashSet", "Optional",
079        "OptionalDouble", "OptionalInt", "OptionalLong",
080        // java.util.stream.*
081        "DoubleStream", "IntStream", "LongStream", "Stream"
082    );
083
084    /** Package names to ignore. */
085    private static final Set<String> DEFAULT_EXCLUDED_PACKAGES = Collections.emptySet();
086
087    /** Pattern to match brackets in a full type name. */
088    private static final Pattern BRACKET_PATTERN = Pattern.compile("\\[[^]]*]");
089
090    /** Specify user-configured regular expressions to ignore classes. */
091    private final List<Pattern> excludeClassesRegexps = new ArrayList<>();
092
093    /** A map of (imported class name -&gt; class name with package) pairs. */
094    private final Map<String, String> importedClassPackages = new HashMap<>();
095
096    /** Stack of class contexts. */
097    private final Deque<ClassContext> classesContexts = new ArrayDeque<>();
098
099    /** 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}