001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2021 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.CheckUtil;
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 String DOT = ".";
054
055    /** Class names to ignore. */
056    private static final Set<String> DEFAULT_EXCLUDED_CLASSES = Collections.unmodifiableSet(
057        Arrays.stream(new String[] {
058            // reserved type name
059            "var",
060            // primitives
061            "boolean", "byte", "char", "double", "float", "int",
062            "long", "short", "void",
063            // wrappers
064            "Boolean", "Byte", "Character", "Double", "Float",
065            "Integer", "Long", "Short", "Void",
066            // java.lang.*
067            "Object", "Class",
068            "String", "StringBuffer", "StringBuilder",
069            // Exceptions
070            "ArrayIndexOutOfBoundsException", "Exception",
071            "RuntimeException", "IllegalArgumentException",
072            "IllegalStateException", "IndexOutOfBoundsException",
073            "NullPointerException", "Throwable", "SecurityException",
074            "UnsupportedOperationException",
075            // java.util.*
076            "List", "ArrayList", "Deque", "Queue", "LinkedList",
077            "Set", "HashSet", "SortedSet", "TreeSet",
078            "Map", "HashMap", "SortedMap", "TreeMap",
079            "Override", "Deprecated", "SafeVarargs", "SuppressWarnings", "FunctionalInterface",
080            "Collection", "EnumSet", "LinkedHashMap", "LinkedHashSet", "Optional",
081            "OptionalDouble", "OptionalInt", "OptionalLong",
082            // java.util.stream.*
083            "DoubleStream", "IntStream", "LongStream", "Stream",
084        }).collect(Collectors.toSet()));
085
086    /** Package names to ignore. */
087    private static final Set<String> DEFAULT_EXCLUDED_PACKAGES = Collections.emptySet();
088
089    /** Specify user-configured regular expressions to ignore classes. */
090    private final List<Pattern> excludeClassesRegexps = new ArrayList<>();
091
092    /** A map of (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. All excluded packages
103     * should end with a period, so it also appends a dot to a package name.
104     */
105    private Set<String> excludedPackages = DEFAULT_EXCLUDED_PACKAGES;
106
107    /** Specify the maximum threshold allowed. */
108    private int max;
109
110    /** Current file package. */
111    private String packageName;
112
113    /**
114     * Creates new instance of the check.
115     *
116     * @param defaultMax default value for allowed complexity.
117     */
118    protected AbstractClassCouplingCheck(int defaultMax) {
119        max = defaultMax;
120        excludeClassesRegexps.add(CommonUtil.createPattern("^$"));
121    }
122
123    /**
124     * Returns message key we use for log violations.
125     *
126     * @return message key we use for log violations.
127     */
128    protected abstract String getLogMessageId();
129
130    @Override
131    public final int[] getDefaultTokens() {
132        return getRequiredTokens();
133    }
134
135    /**
136     * Setter to specify the maximum threshold allowed.
137     *
138     * @param max allowed complexity.
139     */
140    public final void setMax(int max) {
141        this.max = max;
142    }
143
144    /**
145     * Setter to specify user-configured class names to ignore.
146     *
147     * @param excludedClasses the list of classes to ignore.
148     */
149    public final void setExcludedClasses(String... excludedClasses) {
150        this.excludedClasses =
151            Collections.unmodifiableSet(Arrays.stream(excludedClasses).collect(Collectors.toSet()));
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        excludeClassesRegexps.addAll(Arrays.stream(from.clone())
161                .map(CommonUtil::createPattern)
162                .collect(Collectors.toSet()));
163    }
164
165    /**
166     * Setter to specify user-configured packages to ignore. All excluded packages
167     * should end with a period, so it also appends a dot to a package name.
168     *
169     * @param excludedPackages the list of packages to ignore.
170     * @throws IllegalArgumentException if there are invalid identifiers among the packages.
171     */
172    public final void setExcludedPackages(String... excludedPackages) {
173        final List<String> invalidIdentifiers = Arrays.stream(excludedPackages)
174            .filter(excludedPackageName -> !CommonUtil.isName(excludedPackageName))
175            .collect(Collectors.toList());
176        if (!invalidIdentifiers.isEmpty()) {
177            throw new IllegalArgumentException(
178                "the following values are not valid identifiers: "
179                    + invalidIdentifiers.stream().collect(Collectors.joining(", ", "[", "]")));
180        }
181
182        this.excludedPackages = Collections.unmodifiableSet(
183            Arrays.stream(excludedPackages).collect(Collectors.toSet()));
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            final String fullTypeName = CheckUtil.createFullType(ast).getText();
377            addReferencedClassName(fullTypeName);
378        }
379
380        /**
381         * Visits NEW.
382         *
383         * @param ast NEW to process.
384         */
385        public void visitLiteralNew(DetailAST ast) {
386            addReferencedClassName(ast.getFirstChild());
387        }
388
389        /**
390         * Adds new referenced class.
391         *
392         * @param ast a node which represents referenced class.
393         */
394        private void addReferencedClassName(DetailAST ast) {
395            final String fullIdentName = FullIdent.createFullIdent(ast).getText();
396            addReferencedClassName(fullIdentName);
397        }
398
399        /**
400         * Adds new referenced class.
401         *
402         * @param referencedClassName class name of the referenced class.
403         */
404        private void addReferencedClassName(String referencedClassName) {
405            if (isSignificant(referencedClassName)) {
406                referencedClassNames.add(referencedClassName);
407            }
408        }
409
410        /** Checks if coupling less than allowed or not. */
411        public void checkCoupling() {
412            referencedClassNames.remove(className);
413            referencedClassNames.remove(packageName + DOT + className);
414
415            if (referencedClassNames.size() > max) {
416                log(classAst, getLogMessageId(),
417                        referencedClassNames.size(), max,
418                        referencedClassNames.toString());
419            }
420        }
421
422        /**
423         * Checks if given class shouldn't be ignored and not from java.lang.
424         *
425         * @param candidateClassName class to check.
426         * @return true if we should count this class.
427         */
428        private boolean isSignificant(String candidateClassName) {
429            return !excludedClasses.contains(candidateClassName)
430                && !isFromExcludedPackage(candidateClassName)
431                && !isExcludedClassRegexp(candidateClassName);
432        }
433
434        /**
435         * Checks if given class should be ignored as it belongs to excluded package.
436         *
437         * @param candidateClassName class to check
438         * @return true if we should not count this class.
439         */
440        private boolean isFromExcludedPackage(String candidateClassName) {
441            String classNameWithPackage = candidateClassName;
442            if (!candidateClassName.contains(DOT)) {
443                classNameWithPackage = getClassNameWithPackage(candidateClassName)
444                    .orElse("");
445            }
446            boolean isFromExcludedPackage = false;
447            if (classNameWithPackage.contains(DOT)) {
448                final int lastDotIndex = classNameWithPackage.lastIndexOf(DOT);
449                final String candidatePackageName =
450                    classNameWithPackage.substring(0, lastDotIndex);
451                isFromExcludedPackage = candidatePackageName.startsWith("java.lang")
452                    || excludedPackages.contains(candidatePackageName);
453            }
454            return isFromExcludedPackage;
455        }
456
457        /**
458         * Retrieves class name with packages. Uses previously registered imports to
459         * get the full class name.
460         *
461         * @param examineClassName Class name to be retrieved.
462         * @return Class name with package name, if found, {@link Optional#empty()} otherwise.
463         */
464        private Optional<String> getClassNameWithPackage(String examineClassName) {
465            return Optional.ofNullable(importedClassPackages.get(examineClassName));
466        }
467
468        /**
469         * Checks if given class should be ignored as it belongs to excluded class regexp.
470         *
471         * @param candidateClassName class to check.
472         * @return true if we should not count this class.
473         */
474        private boolean isExcludedClassRegexp(String candidateClassName) {
475            boolean result = false;
476            for (Pattern pattern : excludeClassesRegexps) {
477                if (pattern.matcher(candidateClassName).matches()) {
478                    result = true;
479                    break;
480                }
481            }
482            return result;
483        }
484
485    }
486
487}