001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 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;
035import java.util.stream.Collectors;
036
037import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
038import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
039import com.puppycrawl.tools.checkstyle.api.DetailAST;
040import com.puppycrawl.tools.checkstyle.api.FullIdent;
041import com.puppycrawl.tools.checkstyle.api.TokenTypes;
042import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
043import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
044
045/**
046 * Base class for coupling calculation.
047 *
048 */
049@FileStatefulCheck
050public abstract class AbstractClassCouplingCheck extends AbstractCheck {
051
052    /** A package separator - "." */
053    private static final char DOT = '.';
054
055    /** Class names to ignore. */
056    private static final Set<String> DEFAULT_EXCLUDED_CLASSES = Set.of(
057        // reserved type name
058        "var",
059        // primitives
060        "boolean", "byte", "char", "double", "float", "int",
061        "long", "short", "void",
062        // wrappers
063        "Boolean", "Byte", "Character", "Double", "Float",
064        "Integer", "Long", "Short", "Void",
065        // java.lang.*
066        "Object", "Class",
067        "String", "StringBuffer", "StringBuilder",
068        // Exceptions
069        "ArrayIndexOutOfBoundsException", "Exception",
070        "RuntimeException", "IllegalArgumentException",
071        "IllegalStateException", "IndexOutOfBoundsException",
072        "NullPointerException", "Throwable", "SecurityException",
073        "UnsupportedOperationException",
074        // java.util.*
075        "List", "ArrayList", "Deque", "Queue", "LinkedList",
076        "Set", "HashSet", "SortedSet", "TreeSet",
077        "Map", "HashMap", "SortedMap", "TreeMap",
078        "Override", "Deprecated", "SafeVarargs", "SuppressWarnings", "FunctionalInterface",
079        "Collection", "EnumSet", "LinkedHashMap", "LinkedHashSet", "Optional",
080        "OptionalDouble", "OptionalInt", "OptionalLong",
081        // java.util.stream.*
082        "DoubleStream", "IntStream", "LongStream", "Stream"
083    );
084
085    /** Package names to ignore. */
086    private static final Set<String> DEFAULT_EXCLUDED_PACKAGES = Collections.emptySet();
087
088    /** Pattern to match brackets in a full type name. */
089    private static final Pattern BRACKET_PATTERN = Pattern.compile("\\[[^]]*]");
090
091    /** Specify user-configured regular expressions to ignore classes. */
092    private final List<Pattern> excludeClassesRegexps = new ArrayList<>();
093
094    /** A map of (imported class name -&gt; class name with package) pairs. */
095    private final Map<String, String> importedClassPackages = new HashMap<>();
096
097    /** Stack of class contexts. */
098    private final Deque<ClassContext> classesContexts = new ArrayDeque<>();
099
100    /** Specify user-configured class names to ignore. */
101    private Set<String> excludedClasses = DEFAULT_EXCLUDED_CLASSES;
102
103    /**
104     * Specify user-configured packages to ignore.
105     */
106    private Set<String> excludedPackages = DEFAULT_EXCLUDED_PACKAGES;
107
108    /** Specify the maximum threshold allowed. */
109    private int max;
110
111    /** Current file package. */
112    private String packageName;
113
114    /**
115     * Creates new instance of the check.
116     *
117     * @param defaultMax default value for allowed complexity.
118     */
119    protected AbstractClassCouplingCheck(int defaultMax) {
120        max = defaultMax;
121        excludeClassesRegexps.add(CommonUtil.createPattern("^$"));
122    }
123
124    /**
125     * Returns message key we use for log violations.
126     *
127     * @return message key we use for log violations.
128     */
129    protected abstract String getLogMessageId();
130
131    @Override
132    public final int[] getDefaultTokens() {
133        return getRequiredTokens();
134    }
135
136    /**
137     * Setter to specify the maximum threshold allowed.
138     *
139     * @param max allowed complexity.
140     */
141    public final void setMax(int max) {
142        this.max = max;
143    }
144
145    /**
146     * Setter to specify user-configured class names to ignore.
147     *
148     * @param excludedClasses classes to ignore.
149     */
150    public final void setExcludedClasses(String... excludedClasses) {
151        this.excludedClasses = Set.of(excludedClasses);
152    }
153
154    /**
155     * Setter to specify user-configured regular expressions to ignore classes.
156     *
157     * @param from array representing regular expressions of classes to ignore.
158     */
159    public void setExcludeClassesRegexps(String... from) {
160        Arrays.stream(from)
161                .map(CommonUtil::createPattern)
162                .forEach(excludeClassesRegexps::add);
163    }
164
165    /**
166     * Setter to specify user-configured packages to ignore.
167     *
168     * @param excludedPackages packages to ignore.
169     * @throws IllegalArgumentException if there are invalid identifiers among the packages.
170     */
171    public final void setExcludedPackages(String... excludedPackages) {
172        final List<String> invalidIdentifiers = Arrays.stream(excludedPackages)
173            .filter(Predicate.not(CommonUtil::isName))
174            .collect(Collectors.toUnmodifiableList());
175        if (!invalidIdentifiers.isEmpty()) {
176            throw new IllegalArgumentException(
177                "the following values are not valid identifiers: " + invalidIdentifiers);
178        }
179
180        this.excludedPackages = Set.of(excludedPackages);
181    }
182
183    @Override
184    public final void beginTree(DetailAST ast) {
185        importedClassPackages.clear();
186        classesContexts.clear();
187        classesContexts.push(new ClassContext("", null));
188        packageName = "";
189    }
190
191    @Override
192    public void visitToken(DetailAST ast) {
193        switch (ast.getType()) {
194            case TokenTypes.PACKAGE_DEF:
195                visitPackageDef(ast);
196                break;
197            case TokenTypes.IMPORT:
198                registerImport(ast);
199                break;
200            case TokenTypes.CLASS_DEF:
201            case TokenTypes.INTERFACE_DEF:
202            case TokenTypes.ANNOTATION_DEF:
203            case TokenTypes.ENUM_DEF:
204            case TokenTypes.RECORD_DEF:
205                visitClassDef(ast);
206                break;
207            case TokenTypes.EXTENDS_CLAUSE:
208            case TokenTypes.IMPLEMENTS_CLAUSE:
209            case TokenTypes.TYPE:
210                visitType(ast);
211                break;
212            case TokenTypes.LITERAL_NEW:
213                visitLiteralNew(ast);
214                break;
215            case TokenTypes.LITERAL_THROWS:
216                visitLiteralThrows(ast);
217                break;
218            case TokenTypes.ANNOTATION:
219                visitAnnotationType(ast);
220                break;
221            default:
222                throw new IllegalArgumentException("Unknown type: " + ast);
223        }
224    }
225
226    @Override
227    public void leaveToken(DetailAST ast) {
228        if (TokenUtil.isTypeDeclaration(ast.getType())) {
229            leaveClassDef();
230        }
231    }
232
233    /**
234     * Stores package of current class we check.
235     *
236     * @param pkg package definition.
237     */
238    private void visitPackageDef(DetailAST pkg) {
239        final FullIdent ident = FullIdent.createFullIdent(pkg.getLastChild().getPreviousSibling());
240        packageName = ident.getText();
241    }
242
243    /**
244     * Creates new context for a given class.
245     *
246     * @param classDef class definition node.
247     */
248    private void visitClassDef(DetailAST classDef) {
249        final String className = classDef.findFirstToken(TokenTypes.IDENT).getText();
250        createNewClassContext(className, classDef);
251    }
252
253    /** Restores previous context. */
254    private void leaveClassDef() {
255        checkCurrentClassAndRestorePrevious();
256    }
257
258    /**
259     * Registers given import. This allows us to track imported classes.
260     *
261     * @param imp import definition.
262     */
263    private void registerImport(DetailAST imp) {
264        final FullIdent ident = FullIdent.createFullIdent(
265            imp.getLastChild().getPreviousSibling());
266        final String fullName = ident.getText();
267        final int lastDot = fullName.lastIndexOf(DOT);
268        importedClassPackages.put(fullName.substring(lastDot + 1), fullName);
269    }
270
271    /**
272     * Creates new inner class context with given name and location.
273     *
274     * @param className The class name.
275     * @param ast The class ast.
276     */
277    private void createNewClassContext(String className, DetailAST ast) {
278        classesContexts.push(new ClassContext(className, ast));
279    }
280
281    /** Restores previous context. */
282    private void checkCurrentClassAndRestorePrevious() {
283        classesContexts.pop().checkCoupling();
284    }
285
286    /**
287     * Visits type token for the current class context.
288     *
289     * @param ast TYPE token.
290     */
291    private void visitType(DetailAST ast) {
292        classesContexts.peek().visitType(ast);
293    }
294
295    /**
296     * Visits NEW token for the current class context.
297     *
298     * @param ast NEW token.
299     */
300    private void visitLiteralNew(DetailAST ast) {
301        classesContexts.peek().visitLiteralNew(ast);
302    }
303
304    /**
305     * Visits THROWS token for the current class context.
306     *
307     * @param ast THROWS token.
308     */
309    private void visitLiteralThrows(DetailAST ast) {
310        classesContexts.peek().visitLiteralThrows(ast);
311    }
312
313    /**
314     * Visit ANNOTATION literal and get its type to referenced classes of context.
315     *
316     * @param annotationAST Annotation ast.
317     */
318    private void visitAnnotationType(DetailAST annotationAST) {
319        final DetailAST children = annotationAST.getFirstChild();
320        final DetailAST type = children.getNextSibling();
321        classesContexts.peek().addReferencedClassName(type.getText());
322    }
323
324    /**
325     * Encapsulates information about class coupling.
326     *
327     */
328    private final class ClassContext {
329
330        /**
331         * Set of referenced classes.
332         * Sorted by name for predictable violation messages in unit tests.
333         */
334        private final Set<String> referencedClassNames = new TreeSet<>();
335        /** Own class name. */
336        private final String className;
337        /* Location of own class. (Used to log violations) */
338        /** AST of class definition. */
339        private final DetailAST classAst;
340
341        /**
342         * Create new context associated with given class.
343         *
344         * @param className name of the given class.
345         * @param ast ast of class definition.
346         */
347        private ClassContext(String className, DetailAST ast) {
348            this.className = className;
349            classAst = ast;
350        }
351
352        /**
353         * Visits throws clause and collects all exceptions we throw.
354         *
355         * @param literalThrows throws to process.
356         */
357        public void visitLiteralThrows(DetailAST literalThrows) {
358            for (DetailAST childAST = literalThrows.getFirstChild();
359                 childAST != null;
360                 childAST = childAST.getNextSibling()) {
361                if (childAST.getType() != TokenTypes.COMMA) {
362                    addReferencedClassName(childAST);
363                }
364            }
365        }
366
367        /**
368         * Visits type.
369         *
370         * @param ast type to process.
371         */
372        public void visitType(DetailAST ast) {
373            DetailAST child = ast.getFirstChild();
374            while (child != null) {
375                if (TokenUtil.isOfType(child, TokenTypes.IDENT, TokenTypes.DOT)) {
376                    final String fullTypeName = FullIdent.createFullIdent(child).getText();
377                    final String trimmed = BRACKET_PATTERN
378                            .matcher(fullTypeName).replaceAll("");
379                    addReferencedClassName(trimmed);
380                }
381                child = child.getNextSibling();
382            }
383        }
384
385        /**
386         * Visits NEW.
387         *
388         * @param ast NEW to process.
389         */
390        public void visitLiteralNew(DetailAST ast) {
391            addReferencedClassName(ast.getFirstChild());
392        }
393
394        /**
395         * Adds new referenced class.
396         *
397         * @param ast a node which represents referenced class.
398         */
399        private void addReferencedClassName(DetailAST ast) {
400            final String fullIdentName = FullIdent.createFullIdent(ast).getText();
401            final String trimmed = BRACKET_PATTERN
402                    .matcher(fullIdentName).replaceAll("");
403            addReferencedClassName(trimmed);
404        }
405
406        /**
407         * Adds new referenced class.
408         *
409         * @param referencedClassName class name of the referenced class.
410         */
411        private void addReferencedClassName(String referencedClassName) {
412            if (isSignificant(referencedClassName)) {
413                referencedClassNames.add(referencedClassName);
414            }
415        }
416
417        /** Checks if coupling less than allowed or not. */
418        public void checkCoupling() {
419            referencedClassNames.remove(className);
420            referencedClassNames.remove(packageName + DOT + className);
421
422            if (referencedClassNames.size() > max) {
423                log(classAst, getLogMessageId(),
424                        referencedClassNames.size(), max,
425                        referencedClassNames.toString());
426            }
427        }
428
429        /**
430         * Checks if given class shouldn't be ignored and not from java.lang.
431         *
432         * @param candidateClassName class to check.
433         * @return true if we should count this class.
434         */
435        private boolean isSignificant(String candidateClassName) {
436            return !excludedClasses.contains(candidateClassName)
437                && !isFromExcludedPackage(candidateClassName)
438                && !isExcludedClassRegexp(candidateClassName);
439        }
440
441        /**
442         * Checks if given class should be ignored as it belongs to excluded package.
443         *
444         * @param candidateClassName class to check
445         * @return true if we should not count this class.
446         */
447        private boolean isFromExcludedPackage(String candidateClassName) {
448            String classNameWithPackage = candidateClassName;
449            if (candidateClassName.indexOf(DOT) == -1) {
450                classNameWithPackage = getClassNameWithPackage(candidateClassName)
451                    .orElse("");
452            }
453            boolean isFromExcludedPackage = false;
454            if (classNameWithPackage.indexOf(DOT) != -1) {
455                final int lastDotIndex = classNameWithPackage.lastIndexOf(DOT);
456                final String candidatePackageName =
457                    classNameWithPackage.substring(0, lastDotIndex);
458                isFromExcludedPackage = candidatePackageName.startsWith("java.lang")
459                    || excludedPackages.contains(candidatePackageName);
460            }
461            return isFromExcludedPackage;
462        }
463
464        /**
465         * Retrieves class name with packages. Uses previously registered imports to
466         * get the full class name.
467         *
468         * @param examineClassName Class name to be retrieved.
469         * @return Class name with package name, if found, {@link Optional#empty()} otherwise.
470         */
471        private Optional<String> getClassNameWithPackage(String examineClassName) {
472            return Optional.ofNullable(importedClassPackages.get(examineClassName));
473        }
474
475        /**
476         * Checks if given class should be ignored as it belongs to excluded class regexp.
477         *
478         * @param candidateClassName class to check.
479         * @return true if we should not count this class.
480         */
481        private boolean isExcludedClassRegexp(String candidateClassName) {
482            boolean result = false;
483            for (Pattern pattern : excludeClassesRegexps) {
484                if (pattern.matcher(candidateClassName).matches()) {
485                    result = true;
486                    break;
487                }
488            }
489            return result;
490        }
491
492    }
493
494}