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