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