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