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