001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2022 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                .distinct()
164                .forEach(excludeClassesRegexps::add);
165    }
166
167    /**
168     * Setter to specify user-configured packages to ignore. All excluded packages
169     * should end with a period, so it also appends a dot to a package name.
170     *
171     * @param excludedPackages packages to ignore.
172     * @throws IllegalArgumentException if there are invalid identifiers among the packages.
173     */
174    public final void setExcludedPackages(String... excludedPackages) {
175        final List<String> invalidIdentifiers = Arrays.stream(excludedPackages)
176            .filter(Predicate.not(CommonUtil::isName))
177            .collect(Collectors.toList());
178        if (!invalidIdentifiers.isEmpty()) {
179            throw new IllegalArgumentException(
180                "the following values are not valid identifiers: " + invalidIdentifiers);
181        }
182
183        this.excludedPackages = Set.of(excludedPackages);
184    }
185
186    @Override
187    public final void beginTree(DetailAST ast) {
188        importedClassPackages.clear();
189        classesContexts.clear();
190        classesContexts.push(new ClassContext("", null));
191        packageName = "";
192    }
193
194    @Override
195    public void visitToken(DetailAST ast) {
196        switch (ast.getType()) {
197            case TokenTypes.PACKAGE_DEF:
198                visitPackageDef(ast);
199                break;
200            case TokenTypes.IMPORT:
201                registerImport(ast);
202                break;
203            case TokenTypes.CLASS_DEF:
204            case TokenTypes.INTERFACE_DEF:
205            case TokenTypes.ANNOTATION_DEF:
206            case TokenTypes.ENUM_DEF:
207            case TokenTypes.RECORD_DEF:
208                visitClassDef(ast);
209                break;
210            case TokenTypes.EXTENDS_CLAUSE:
211            case TokenTypes.IMPLEMENTS_CLAUSE:
212            case TokenTypes.TYPE:
213                visitType(ast);
214                break;
215            case TokenTypes.LITERAL_NEW:
216                visitLiteralNew(ast);
217                break;
218            case TokenTypes.LITERAL_THROWS:
219                visitLiteralThrows(ast);
220                break;
221            case TokenTypes.ANNOTATION:
222                visitAnnotationType(ast);
223                break;
224            default:
225                throw new IllegalArgumentException("Unknown type: " + ast);
226        }
227    }
228
229    @Override
230    public void leaveToken(DetailAST ast) {
231        if (TokenUtil.isTypeDeclaration(ast.getType())) {
232            leaveClassDef();
233        }
234    }
235
236    /**
237     * Stores package of current class we check.
238     *
239     * @param pkg package definition.
240     */
241    private void visitPackageDef(DetailAST pkg) {
242        final FullIdent ident = FullIdent.createFullIdent(pkg.getLastChild().getPreviousSibling());
243        packageName = ident.getText();
244    }
245
246    /**
247     * Creates new context for a given class.
248     *
249     * @param classDef class definition node.
250     */
251    private void visitClassDef(DetailAST classDef) {
252        final String className = classDef.findFirstToken(TokenTypes.IDENT).getText();
253        createNewClassContext(className, classDef);
254    }
255
256    /** Restores previous context. */
257    private void leaveClassDef() {
258        checkCurrentClassAndRestorePrevious();
259    }
260
261    /**
262     * Registers given import. This allows us to track imported classes.
263     *
264     * @param imp import definition.
265     */
266    private void registerImport(DetailAST imp) {
267        final FullIdent ident = FullIdent.createFullIdent(
268            imp.getLastChild().getPreviousSibling());
269        final String fullName = ident.getText();
270        final int lastDot = fullName.lastIndexOf(DOT);
271        importedClassPackages.put(fullName.substring(lastDot + 1), fullName);
272    }
273
274    /**
275     * Creates new inner class context with given name and location.
276     *
277     * @param className The class name.
278     * @param ast The class ast.
279     */
280    private void createNewClassContext(String className, DetailAST ast) {
281        classesContexts.push(new ClassContext(className, ast));
282    }
283
284    /** Restores previous context. */
285    private void checkCurrentClassAndRestorePrevious() {
286        classesContexts.pop().checkCoupling();
287    }
288
289    /**
290     * Visits type token for the current class context.
291     *
292     * @param ast TYPE token.
293     */
294    private void visitType(DetailAST ast) {
295        classesContexts.peek().visitType(ast);
296    }
297
298    /**
299     * Visits NEW token for the current class context.
300     *
301     * @param ast NEW token.
302     */
303    private void visitLiteralNew(DetailAST ast) {
304        classesContexts.peek().visitLiteralNew(ast);
305    }
306
307    /**
308     * Visits THROWS token for the current class context.
309     *
310     * @param ast THROWS token.
311     */
312    private void visitLiteralThrows(DetailAST ast) {
313        classesContexts.peek().visitLiteralThrows(ast);
314    }
315
316    /**
317     * Visit ANNOTATION literal and get its type to referenced classes of context.
318     *
319     * @param annotationAST Annotation ast.
320     */
321    private void visitAnnotationType(DetailAST annotationAST) {
322        final DetailAST children = annotationAST.getFirstChild();
323        final DetailAST type = children.getNextSibling();
324        classesContexts.peek().addReferencedClassName(type.getText());
325    }
326
327    /**
328     * Encapsulates information about class coupling.
329     *
330     */
331    private class ClassContext {
332
333        /**
334         * Set of referenced classes.
335         * Sorted by name for predictable violation messages in unit tests.
336         */
337        private final Set<String> referencedClassNames = new TreeSet<>();
338        /** Own class name. */
339        private final String className;
340        /* Location of own class. (Used to log violations) */
341        /** AST of class definition. */
342        private final DetailAST classAst;
343
344        /**
345         * Create new context associated with given class.
346         *
347         * @param className name of the given class.
348         * @param ast ast of class definition.
349         */
350        /* package */ ClassContext(String className, DetailAST ast) {
351            this.className = className;
352            classAst = ast;
353        }
354
355        /**
356         * Visits throws clause and collects all exceptions we throw.
357         *
358         * @param literalThrows throws to process.
359         */
360        public void visitLiteralThrows(DetailAST literalThrows) {
361            for (DetailAST childAST = literalThrows.getFirstChild();
362                 childAST != null;
363                 childAST = childAST.getNextSibling()) {
364                if (childAST.getType() != TokenTypes.COMMA) {
365                    addReferencedClassName(childAST);
366                }
367            }
368        }
369
370        /**
371         * Visits type.
372         *
373         * @param ast type to process.
374         */
375        public void visitType(DetailAST ast) {
376            DetailAST child = ast.getFirstChild();
377            while (child != null) {
378                if (TokenUtil.isOfType(child, TokenTypes.IDENT, TokenTypes.DOT)) {
379                    final String fullTypeName = FullIdent.createFullIdent(child).getText();
380                    final String trimmed = BRACKET_PATTERN
381                            .matcher(fullTypeName).replaceAll("");
382                    addReferencedClassName(trimmed);
383                }
384                child = child.getNextSibling();
385            }
386        }
387
388        /**
389         * Visits NEW.
390         *
391         * @param ast NEW to process.
392         */
393        public void visitLiteralNew(DetailAST ast) {
394            addReferencedClassName(ast.getFirstChild());
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    }
496
497}