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