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