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