001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2017 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.api.AbstractCheck;
037import com.puppycrawl.tools.checkstyle.api.DetailAST;
038import com.puppycrawl.tools.checkstyle.api.FullIdent;
039import com.puppycrawl.tools.checkstyle.api.TokenTypes;
040import com.puppycrawl.tools.checkstyle.utils.CheckUtils;
041import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
042
043/**
044 * Base class for coupling calculation.
045 *
046 * @author <a href="mailto:simon@redhillconsulting.com.au">Simon Harris</a>
047 * @author o_sukhodolsky
048 */
049public abstract class AbstractClassCouplingCheck extends AbstractCheck {
050    /** A package separator - "." */
051    private static final String DOT = ".";
052
053    /** Class names to ignore. */
054    private static final Set<String> DEFAULT_EXCLUDED_CLASSES = Collections.unmodifiableSet(
055        Arrays.stream(new String[] {
056            // primitives
057            "boolean", "byte", "char", "double", "float", "int",
058            "long", "short", "void",
059            // wrappers
060            "Boolean", "Byte", "Character", "Double", "Float",
061            "Integer", "Long", "Short", "Void",
062            // java.lang.*
063            "Object", "Class",
064            "String", "StringBuffer", "StringBuilder",
065            // Exceptions
066            "ArrayIndexOutOfBoundsException", "Exception",
067            "RuntimeException", "IllegalArgumentException",
068            "IllegalStateException", "IndexOutOfBoundsException",
069            "NullPointerException", "Throwable", "SecurityException",
070            "UnsupportedOperationException",
071            // java.util.*
072            "List", "ArrayList", "Deque", "Queue", "LinkedList",
073            "Set", "HashSet", "SortedSet", "TreeSet",
074            "Map", "HashMap", "SortedMap", "TreeMap",
075        }).collect(Collectors.toSet()));
076
077    /** Package names to ignore. */
078    private static final Set<String> DEFAULT_EXCLUDED_PACKAGES = Collections.emptySet();
079
080    /** User-configured regular expressions to ignore classes. */
081    private final List<Pattern> excludeClassesRegexps = new ArrayList<>();
082
083    /** User-configured class names to ignore. */
084    private Set<String> excludedClasses = DEFAULT_EXCLUDED_CLASSES;
085    /** User-configured package names to ignore. */
086    private Set<String> excludedPackages = DEFAULT_EXCLUDED_PACKAGES;
087    /** Allowed complexity. */
088    private int max;
089
090    /** Current file context. */
091    private FileContext fileContext;
092
093    /**
094     * Creates new instance of the check.
095     * @param defaultMax default value for allowed complexity.
096     */
097    protected AbstractClassCouplingCheck(int defaultMax) {
098        max = defaultMax;
099        excludeClassesRegexps.add(CommonUtils.createPattern("^$"));
100    }
101
102    /**
103     * @return message key we use for log violations.
104     */
105    protected abstract String getLogMessageId();
106
107    @Override
108    public final int[] getDefaultTokens() {
109        return getRequiredTokens();
110    }
111
112    /**
113     * @return allowed complexity.
114     */
115    public final int getMax() {
116        return max;
117    }
118
119    /**
120     * Sets maximum allowed complexity.
121     * @param max allowed complexity.
122     */
123    public final void setMax(int max) {
124        this.max = max;
125    }
126
127    /**
128     * Sets user-excluded classes to ignore.
129     * @param excludedClasses the list of classes to ignore.
130     */
131    public final void setExcludedClasses(String... excludedClasses) {
132        this.excludedClasses =
133            Collections.unmodifiableSet(Arrays.stream(excludedClasses).collect(Collectors.toSet()));
134    }
135
136    /**
137     * Sets user-excluded regular expression of classes to ignore.
138     * @param from array representing regular expressions of classes to ignore.
139     */
140    public void setExcludeClassesRegexps(String... from) {
141        excludeClassesRegexps.clear();
142        excludeClassesRegexps.addAll(Arrays.stream(from.clone())
143                .map(CommonUtils::createPattern)
144                .collect(Collectors.toSet()));
145    }
146
147    /**
148     * Sets user-excluded pakcages to ignore. All exlcuded packages should end with a period,
149     * so it also appends a dot to a package name.
150     * @param excludedPackages the list of packages to ignore.
151     */
152    public final void setExcludedPackages(String... excludedPackages) {
153        final List<String> invalidIdentifiers = Arrays.stream(excludedPackages)
154            .filter(x -> !CommonUtils.isName(x))
155            .collect(Collectors.toList());
156        if (!invalidIdentifiers.isEmpty()) {
157            throw new IllegalArgumentException(
158                "the following values are not valid identifiers: "
159                    + invalidIdentifiers.stream().collect(Collectors.joining(", ", "[", "]")));
160        }
161
162        this.excludedPackages = Collections.unmodifiableSet(
163            Arrays.stream(excludedPackages).collect(Collectors.toSet()));
164    }
165
166    @Override
167    public final void beginTree(DetailAST ast) {
168        fileContext = new FileContext();
169    }
170
171    @Override
172    public void visitToken(DetailAST ast) {
173        switch (ast.getType()) {
174            case TokenTypes.PACKAGE_DEF:
175                visitPackageDef(ast);
176                break;
177            case TokenTypes.IMPORT:
178                fileContext.registerImport(ast);
179                break;
180            case TokenTypes.CLASS_DEF:
181            case TokenTypes.INTERFACE_DEF:
182            case TokenTypes.ANNOTATION_DEF:
183            case TokenTypes.ENUM_DEF:
184                visitClassDef(ast);
185                break;
186            case TokenTypes.TYPE:
187                fileContext.visitType(ast);
188                break;
189            case TokenTypes.LITERAL_NEW:
190                fileContext.visitLiteralNew(ast);
191                break;
192            case TokenTypes.LITERAL_THROWS:
193                fileContext.visitLiteralThrows(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.getLineNo(), classDef.getColumnNo());
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     */
240    private class FileContext {
241        /** A map of (imported class name -> class name with package) pairs. */
242        private final Map<String, String> importedClassPackage = new HashMap<>();
243
244        /** Stack of class contexts. */
245        private final Deque<ClassContext> classesContexts = new ArrayDeque<>();
246
247        /** Current file package. */
248        private String packageName = "";
249
250        /** Current context. */
251        private ClassContext classContext = new ClassContext(this, "", 0, 0);
252
253        /**
254         * Retrieves current file package name.
255         * @return Package name.
256         */
257        public String getPackageName() {
258            return packageName;
259        }
260
261        /**
262         * Sets current context package name.
263         * @param packageName Package name to be set.
264         */
265        public void setPackageName(String packageName) {
266            this.packageName = packageName;
267        }
268
269        /**
270         * Registers given import. This allows us to track imported classes.
271         * @param imp import definition.
272         */
273        public void registerImport(DetailAST imp) {
274            final FullIdent ident = FullIdent.createFullIdent(
275                imp.getLastChild().getPreviousSibling());
276            final String fullName = ident.getText();
277            if (fullName.charAt(fullName.length() - 1) != '*') {
278                final int lastDot = fullName.lastIndexOf(DOT);
279                importedClassPackage.put(fullName.substring(lastDot + 1), fullName);
280            }
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 lineNo The class line number.
297         * @param columnNo The class column number.
298         */
299        public void createNewClassContext(String className, int lineNo, int columnNo) {
300            classesContexts.push(classContext);
301            classContext = new ClassContext(this, className, lineNo, columnNo);
302        }
303
304        /** Restores previous context. */
305        public void checkCurrentClassAndRestorePrevious() {
306            classContext.checkCoupling();
307            classContext = classesContexts.pop();
308        }
309
310        /**
311         * Visits type token for the current class context.
312         * @param ast TYPE token.
313         */
314        public void visitType(DetailAST ast) {
315            classContext.visitType(ast);
316        }
317
318        /**
319         * Visits NEW token for the current class context.
320         * @param ast NEW token.
321         */
322        public void visitLiteralNew(DetailAST ast) {
323            classContext.visitLiteralNew(ast);
324        }
325
326        /**
327         * Visits THROWS token for the current class context.
328         * @param ast THROWS token.
329         */
330        public void visitLiteralThrows(DetailAST ast) {
331            classContext.visitLiteralThrows(ast);
332        }
333    }
334
335    /**
336     * Encapsulates information about class coupling.
337     *
338     * @author <a href="mailto:simon@redhillconsulting.com.au">Simon Harris</a>
339     * @author o_sukhodolsky
340     */
341    private class ClassContext {
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(), getMax(),
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}