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:
192                visitPackageDef(ast);
193                break;
194            case TokenTypes.IMPORT:
195                registerImport(ast);
196                break;
197            case TokenTypes.CLASS_DEF:
198            case TokenTypes.INTERFACE_DEF:
199            case TokenTypes.ANNOTATION_DEF:
200            case TokenTypes.ENUM_DEF:
201            case TokenTypes.RECORD_DEF:
202                visitClassDef(ast);
203                break;
204            case TokenTypes.EXTENDS_CLAUSE:
205            case TokenTypes.IMPLEMENTS_CLAUSE:
206            case TokenTypes.TYPE:
207                visitType(ast);
208                break;
209            case TokenTypes.LITERAL_NEW:
210                visitLiteralNew(ast);
211                break;
212            case TokenTypes.LITERAL_THROWS:
213                visitLiteralThrows(ast);
214                break;
215            case TokenTypes.ANNOTATION:
216                visitAnnotationType(ast);
217                break;
218            default:
219                throw new IllegalArgumentException("Unknown type: " + ast);
220        }
221    }
222
223    @Override
224    public void leaveToken(DetailAST ast) {
225        if (TokenUtil.isTypeDeclaration(ast.getType())) {
226            leaveClassDef();
227        }
228    }
229
230    /**
231     * Stores package of current class we check.
232     *
233     * @param pkg package definition.
234     */
235    private void visitPackageDef(DetailAST pkg) {
236        final FullIdent ident = FullIdent.createFullIdent(pkg.getLastChild().getPreviousSibling());
237        packageName = ident.getText();
238    }
239
240    /**
241     * Creates new context for a given class.
242     *
243     * @param classDef class definition node.
244     */
245    private void visitClassDef(DetailAST classDef) {
246        final String className = classDef.findFirstToken(TokenTypes.IDENT).getText();
247        createNewClassContext(className, classDef);
248    }
249
250    /** Restores previous context. */
251    private void leaveClassDef() {
252        checkCurrentClassAndRestorePrevious();
253    }
254
255    /**
256     * Registers given import. This allows us to track imported classes.
257     *
258     * @param imp import definition.
259     */
260    private void registerImport(DetailAST imp) {
261        final FullIdent ident = FullIdent.createFullIdent(
262            imp.getLastChild().getPreviousSibling());
263        final String fullName = ident.getText();
264        final int lastDot = fullName.lastIndexOf(DOT);
265        importedClassPackages.put(fullName.substring(lastDot + 1), fullName);
266    }
267
268    /**
269     * Creates new inner class context with given name and location.
270     *
271     * @param className The class name.
272     * @param ast The class ast.
273     */
274    private void createNewClassContext(String className, DetailAST ast) {
275        classesContexts.push(new ClassContext(className, ast));
276    }
277
278    /** Restores previous context. */
279    private void checkCurrentClassAndRestorePrevious() {
280        classesContexts.pop().checkCoupling();
281    }
282
283    /**
284     * Visits type token for the current class context.
285     *
286     * @param ast TYPE token.
287     */
288    private void visitType(DetailAST ast) {
289        classesContexts.peek().visitType(ast);
290    }
291
292    /**
293     * Visits NEW token for the current class context.
294     *
295     * @param ast NEW token.
296     */
297    private void visitLiteralNew(DetailAST ast) {
298        classesContexts.peek().visitLiteralNew(ast);
299    }
300
301    /**
302     * Visits THROWS token for the current class context.
303     *
304     * @param ast THROWS token.
305     */
306    private void visitLiteralThrows(DetailAST ast) {
307        classesContexts.peek().visitLiteralThrows(ast);
308    }
309
310    /**
311     * Visit ANNOTATION literal and get its type to referenced classes of context.
312     *
313     * @param annotationAST Annotation ast.
314     */
315    private void visitAnnotationType(DetailAST annotationAST) {
316        final DetailAST children = annotationAST.getFirstChild();
317        final DetailAST type = children.getNextSibling();
318        classesContexts.peek().addReferencedClassName(type.getText());
319    }
320
321    /**
322     * Encapsulates information about class coupling.
323     *
324     */
325    private final class ClassContext {
326
327        /**
328         * Set of referenced classes.
329         * Sorted by name for predictable violation messages in unit tests.
330         */
331        private final Set<String> referencedClassNames = new TreeSet<>();
332        /** Own class name. */
333        private final String className;
334        /* Location of own class. (Used to log violations) */
335        /** AST of class definition. */
336        private final DetailAST classAst;
337
338        /**
339         * Create new context associated with given class.
340         *
341         * @param className name of the given class.
342         * @param ast ast of class definition.
343         */
344        private ClassContext(String className, DetailAST ast) {
345            this.className = className;
346            classAst = ast;
347        }
348
349        /**
350         * Visits throws clause and collects all exceptions we throw.
351         *
352         * @param literalThrows throws to process.
353         */
354        public void visitLiteralThrows(DetailAST literalThrows) {
355            for (DetailAST childAST = literalThrows.getFirstChild();
356                 childAST != null;
357                 childAST = childAST.getNextSibling()) {
358                if (childAST.getType() != TokenTypes.COMMA) {
359                    addReferencedClassName(childAST);
360                }
361            }
362        }
363
364        /**
365         * Visits type.
366         *
367         * @param ast type to process.
368         */
369        public void visitType(DetailAST ast) {
370            DetailAST child = ast.getFirstChild();
371            while (child != null) {
372                if (TokenUtil.isOfType(child, TokenTypes.IDENT, TokenTypes.DOT)) {
373                    final String fullTypeName = FullIdent.createFullIdent(child).getText();
374                    final String trimmed = BRACKET_PATTERN
375                            .matcher(fullTypeName).replaceAll("");
376                    addReferencedClassName(trimmed);
377                }
378                child = child.getNextSibling();
379            }
380        }
381
382        /**
383         * Visits NEW.
384         *
385         * @param ast NEW to process.
386         */
387        public void visitLiteralNew(DetailAST ast) {
388
389            if (ast.getParent().getType() == TokenTypes.METHOD_REF) {
390                addReferencedClassName(ast.getParent().getFirstChild());
391            }
392            else {
393                addReferencedClassName(ast);
394            }
395        }
396
397        /**
398         * Adds new referenced class.
399         *
400         * @param ast a node which represents referenced class.
401         */
402        private void addReferencedClassName(DetailAST ast) {
403            final String fullIdentName = FullIdent.createFullIdent(ast).getText();
404            final String trimmed = BRACKET_PATTERN
405                    .matcher(fullIdentName).replaceAll("");
406            addReferencedClassName(trimmed);
407        }
408
409        /**
410         * Adds new referenced class.
411         *
412         * @param referencedClassName class name of the referenced class.
413         */
414        private void addReferencedClassName(String referencedClassName) {
415            if (isSignificant(referencedClassName)) {
416                referencedClassNames.add(referencedClassName);
417            }
418        }
419
420        /** Checks if coupling less than allowed or not. */
421        public void checkCoupling() {
422            referencedClassNames.remove(className);
423            referencedClassNames.remove(packageName + DOT + className);
424
425            if (referencedClassNames.size() > max) {
426                log(classAst, getLogMessageId(),
427                        referencedClassNames.size(), max,
428                        referencedClassNames.toString());
429            }
430        }
431
432        /**
433         * Checks if given class shouldn't be ignored and not from java.lang.
434         *
435         * @param candidateClassName class to check.
436         * @return true if we should count this class.
437         */
438        private boolean isSignificant(String candidateClassName) {
439            return !excludedClasses.contains(candidateClassName)
440                && !isFromExcludedPackage(candidateClassName)
441                && !isExcludedClassRegexp(candidateClassName);
442        }
443
444        /**
445         * Checks if given class should be ignored as it belongs to excluded package.
446         *
447         * @param candidateClassName class to check
448         * @return true if we should not count this class.
449         */
450        private boolean isFromExcludedPackage(String candidateClassName) {
451            String classNameWithPackage = candidateClassName;
452            if (candidateClassName.indexOf(DOT) == -1) {
453                classNameWithPackage = getClassNameWithPackage(candidateClassName)
454                    .orElse("");
455            }
456            boolean isFromExcludedPackage = false;
457            if (classNameWithPackage.indexOf(DOT) != -1) {
458                final int lastDotIndex = classNameWithPackage.lastIndexOf(DOT);
459                final String candidatePackageName =
460                    classNameWithPackage.substring(0, lastDotIndex);
461                isFromExcludedPackage = candidatePackageName.startsWith("java.lang")
462                    || excludedPackages.contains(candidatePackageName);
463            }
464            return isFromExcludedPackage;
465        }
466
467        /**
468         * Retrieves class name with packages. Uses previously registered imports to
469         * get the full class name.
470         *
471         * @param examineClassName Class name to be retrieved.
472         * @return Class name with package name, if found, {@link Optional#empty()} otherwise.
473         */
474        private Optional<String> getClassNameWithPackage(String examineClassName) {
475            return Optional.ofNullable(importedClassPackages.get(examineClassName));
476        }
477
478        /**
479         * Checks if given class should be ignored as it belongs to excluded class regexp.
480         *
481         * @param candidateClassName class to check.
482         * @return true if we should not count this class.
483         */
484        private boolean isExcludedClassRegexp(String candidateClassName) {
485            boolean result = false;
486            for (Pattern pattern : excludeClassesRegexps) {
487                if (pattern.matcher(candidateClassName).matches()) {
488                    result = true;
489                    break;
490                }
491            }
492            return result;
493        }
494    }
495}