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