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