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