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