001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 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.Deque;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029import java.util.Optional;
030import java.util.Set;
031import java.util.TreeSet;
032import java.util.function.Predicate;
033import java.util.regex.Pattern;
034
035import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
036import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
037import com.puppycrawl.tools.checkstyle.api.DetailAST;
038import com.puppycrawl.tools.checkstyle.api.FullIdent;
039import com.puppycrawl.tools.checkstyle.api.TokenTypes;
040import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
041import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
042
043/**
044 * Base class for coupling calculation.
045 *
046 */
047@FileStatefulCheck
048public abstract class AbstractClassCouplingCheck extends AbstractCheck {
049
050    /** A package separator - ".". */
051    private static final char DOT = '.';
052
053    /** Class names to ignore. */
054    private static final Set<String> DEFAULT_EXCLUDED_CLASSES = Set.of(
055        // reserved type name
056        "var",
057        // primitives
058        "boolean", "byte", "char", "double", "float", "int",
059        "long", "short", "void",
060        // wrappers
061        "Boolean", "Byte", "Character", "Double", "Float",
062        "Integer", "Long", "Short", "Void",
063        // java.lang.*
064        "Object", "Class",
065        "String", "StringBuffer", "StringBuilder",
066        // Exceptions
067        "ArrayIndexOutOfBoundsException", "Exception",
068        "RuntimeException", "IllegalArgumentException",
069        "IllegalStateException", "IndexOutOfBoundsException",
070        "NullPointerException", "Throwable", "SecurityException",
071        "UnsupportedOperationException",
072        // java.util.*
073        "List", "ArrayList", "Deque", "Queue", "LinkedList",
074        "Set", "HashSet", "SortedSet", "TreeSet",
075        "Map", "HashMap", "SortedMap", "TreeMap",
076        "Override", "Deprecated", "SafeVarargs", "SuppressWarnings", "FunctionalInterface",
077        "Collection", "EnumSet", "LinkedHashMap", "LinkedHashSet", "Optional",
078        "OptionalDouble", "OptionalInt", "OptionalLong",
079        // java.util.stream.*
080        "DoubleStream", "IntStream", "LongStream", "Stream"
081    );
082
083    /** Package names to ignore. */
084    private static final Set<String> DEFAULT_EXCLUDED_PACKAGES = Set.of();
085
086    /** Pattern to match brackets in a full type name. */
087    private static final Pattern BRACKET_PATTERN = Pattern.compile("\\[[^]]*]");
088
089    /** Specify user-configured regular expressions to ignore classes. */
090    private final List<Pattern> excludeClassesRegexps = new ArrayList<>();
091
092    /** A map of {@literal (imported class name -> class name with package)} pairs. */
093    private final Map<String, String> importedClassPackages = new HashMap<>();
094
095    /** Stack of class contexts. */
096    private final Deque<ClassContext> classesContexts = new ArrayDeque<>();
097
098    /** Specify user-configured class names to ignore. */
099    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}