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