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