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 */
048@FileStatefulCheck
049public abstract class AbstractClassCouplingCheck extends AbstractCheck {
050
051    /** A package separator - "." */
052    private static final String DOT = ".";
053
054    /** Class names to ignore. */
055    private static final Set<String> DEFAULT_EXCLUDED_CLASSES = Collections.unmodifiableSet(
056        Arrays.stream(new String[] {
057            // primitives
058            "boolean", "byte", "char", "double", "float", "int",
059            "long", "short", "void",
060            // wrappers
061            "Boolean", "Byte", "Character", "Double", "Float",
062            "Integer", "Long", "Short", "Void",
063            // java.lang.*
064            "Object", "Class",
065            "String", "StringBuffer", "StringBuilder",
066            // Exceptions
067            "ArrayIndexOutOfBoundsException", "Exception",
068            "RuntimeException", "IllegalArgumentException",
069            "IllegalStateException", "IndexOutOfBoundsException",
070            "NullPointerException", "Throwable", "SecurityException",
071            "UnsupportedOperationException",
072            // java.util.*
073            "List", "ArrayList", "Deque", "Queue", "LinkedList",
074            "Set", "HashSet", "SortedSet", "TreeSet",
075            "Map", "HashMap", "SortedMap", "TreeMap",
076        }).collect(Collectors.toSet()));
077
078    /** Package names to ignore. */
079    private static final Set<String> DEFAULT_EXCLUDED_PACKAGES = Collections.emptySet();
080
081    /** User-configured regular expressions to ignore classes. */
082    private final List<Pattern> excludeClassesRegexps = new ArrayList<>();
083
084    /** User-configured class names to ignore. */
085    private Set<String> excludedClasses = DEFAULT_EXCLUDED_CLASSES;
086    /** User-configured package names to ignore. */
087    private Set<String> excludedPackages = DEFAULT_EXCLUDED_PACKAGES;
088    /** Allowed complexity. */
089    private int max;
090
091    /** Current file context. */
092    private FileContext fileContext;
093
094    /**
095     * Creates new instance of the check.
096     * @param defaultMax default value for allowed complexity.
097     */
098    protected AbstractClassCouplingCheck(int defaultMax) {
099        max = defaultMax;
100        excludeClassesRegexps.add(CommonUtils.createPattern("^$"));
101    }
102
103    /**
104     * Returns message key we use for log violations.
105     * @return message key we use for log violations.
106     */
107    protected abstract String getLogMessageId();
108
109    @Override
110    public final int[] getDefaultTokens() {
111        return getRequiredTokens();
112    }
113
114    /**
115     * Sets maximum allowed complexity.
116     * @param max allowed complexity.
117     */
118    public final void setMax(int max) {
119        this.max = max;
120    }
121
122    /**
123     * Sets user-excluded classes to ignore.
124     * @param excludedClasses the list of classes to ignore.
125     */
126    public final void setExcludedClasses(String... excludedClasses) {
127        this.excludedClasses =
128            Collections.unmodifiableSet(Arrays.stream(excludedClasses).collect(Collectors.toSet()));
129    }
130
131    /**
132     * Sets user-excluded regular expression of classes to ignore.
133     * @param from array representing regular expressions of classes to ignore.
134     */
135    public void setExcludeClassesRegexps(String... from) {
136        excludeClassesRegexps.addAll(Arrays.stream(from.clone())
137                .map(CommonUtils::createPattern)
138                .collect(Collectors.toSet()));
139    }
140
141    /**
142     * Sets user-excluded packages to ignore. All excluded packages should end with a period,
143     * so it also appends a dot to a package name.
144     * @param excludedPackages the list of packages to ignore.
145     */
146    public final void setExcludedPackages(String... excludedPackages) {
147        final List<String> invalidIdentifiers = Arrays.stream(excludedPackages)
148            .filter(x -> !CommonUtils.isName(x))
149            .collect(Collectors.toList());
150        if (!invalidIdentifiers.isEmpty()) {
151            throw new IllegalArgumentException(
152                "the following values are not valid identifiers: "
153                    + invalidIdentifiers.stream().collect(Collectors.joining(", ", "[", "]")));
154        }
155
156        this.excludedPackages = Collections.unmodifiableSet(
157            Arrays.stream(excludedPackages).collect(Collectors.toSet()));
158    }
159
160    @Override
161    public final void beginTree(DetailAST ast) {
162        fileContext = new FileContext();
163    }
164
165    @Override
166    public void visitToken(DetailAST ast) {
167        switch (ast.getType()) {
168            case TokenTypes.PACKAGE_DEF:
169                visitPackageDef(ast);
170                break;
171            case TokenTypes.IMPORT:
172                fileContext.registerImport(ast);
173                break;
174            case TokenTypes.CLASS_DEF:
175            case TokenTypes.INTERFACE_DEF:
176            case TokenTypes.ANNOTATION_DEF:
177            case TokenTypes.ENUM_DEF:
178                visitClassDef(ast);
179                break;
180            case TokenTypes.TYPE:
181                fileContext.visitType(ast);
182                break;
183            case TokenTypes.LITERAL_NEW:
184                fileContext.visitLiteralNew(ast);
185                break;
186            case TokenTypes.LITERAL_THROWS:
187                fileContext.visitLiteralThrows(ast);
188                break;
189            default:
190                throw new IllegalArgumentException("Unknown type: " + ast);
191        }
192    }
193
194    @Override
195    public void leaveToken(DetailAST ast) {
196        switch (ast.getType()) {
197            case TokenTypes.CLASS_DEF:
198            case TokenTypes.INTERFACE_DEF:
199            case TokenTypes.ANNOTATION_DEF:
200            case TokenTypes.ENUM_DEF:
201                leaveClassDef();
202                break;
203            default:
204                // Do nothing
205        }
206    }
207
208    /**
209     * Stores package of current class we check.
210     * @param pkg package definition.
211     */
212    private void visitPackageDef(DetailAST pkg) {
213        final FullIdent ident = FullIdent.createFullIdent(pkg.getLastChild().getPreviousSibling());
214        fileContext.setPackageName(ident.getText());
215    }
216
217    /**
218     * Creates new context for a given class.
219     * @param classDef class definition node.
220     */
221    private void visitClassDef(DetailAST classDef) {
222        final String className = classDef.findFirstToken(TokenTypes.IDENT).getText();
223        fileContext.createNewClassContext(className, classDef.getLineNo(), classDef.getColumnNo());
224    }
225
226    /** Restores previous context. */
227    private void leaveClassDef() {
228        fileContext.checkCurrentClassAndRestorePrevious();
229    }
230
231    /**
232     * Encapsulates information about classes coupling inside single file.
233     * @noinspection ThisEscapedInObjectConstruction
234     */
235    private class FileContext {
236
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    /**
333     * Encapsulates information about class coupling.
334     *
335     */
336    private class ClassContext {
337
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    }
470
471}