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