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.Arrays;
024import java.util.Collections;
025import java.util.Deque;
026import java.util.Set;
027import java.util.TreeSet;
028import java.util.stream.Collectors;
029import java.util.stream.Stream;
030
031import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
032import com.puppycrawl.tools.checkstyle.api.DetailAST;
033import com.puppycrawl.tools.checkstyle.api.FullIdent;
034import com.puppycrawl.tools.checkstyle.api.TokenTypes;
035import com.puppycrawl.tools.checkstyle.utils.CheckUtils;
036
037/**
038 * Base class for coupling calculation.
039 *
040 * @author <a href="mailto:simon@redhillconsulting.com.au">Simon Harris</a>
041 * @author o_sukhodolsky
042 */
043public abstract class AbstractClassCouplingCheck extends AbstractCheck {
044    /** Class names to ignore. */
045    private static final Set<String> DEFAULT_EXCLUDED_CLASSES = Collections.unmodifiableSet(
046        Stream.of(
047            // primitives
048            "boolean", "byte", "char", "double", "float", "int",
049            "long", "short", "void",
050            // wrappers
051            "Boolean", "Byte", "Character", "Double", "Float",
052            "Integer", "Long", "Short", "Void",
053            // java.lang.*
054            "Object", "Class",
055            "String", "StringBuffer", "StringBuilder",
056            // Exceptions
057            "ArrayIndexOutOfBoundsException", "Exception",
058            "RuntimeException", "IllegalArgumentException",
059            "IllegalStateException", "IndexOutOfBoundsException",
060            "NullPointerException", "Throwable", "SecurityException",
061            "UnsupportedOperationException",
062            // java.util.*
063            "List", "ArrayList", "Deque", "Queue", "LinkedList",
064            "Set", "HashSet", "SortedSet", "TreeSet",
065            "Map", "HashMap", "SortedMap", "TreeMap"
066        ).collect(Collectors.toSet()));
067
068    /** Stack of contexts. */
069    private final Deque<Context> contextStack = new ArrayDeque<>();
070
071    /** User-configured class names to ignore. */
072    private Set<String> excludedClasses = DEFAULT_EXCLUDED_CLASSES;
073    /** Allowed complexity. */
074    private int max;
075    /** Package of the file we check. */
076    private String packageName;
077
078    /** Current context. */
079    private Context context = new Context("", 0, 0);
080
081    /**
082     * Creates new instance of the check.
083     * @param defaultMax default value for allowed complexity.
084     */
085    protected AbstractClassCouplingCheck(int defaultMax) {
086        max = defaultMax;
087    }
088
089    /**
090     * @return message key we use for log violations.
091     */
092    protected abstract String getLogMessageId();
093
094    @Override
095    public final int[] getDefaultTokens() {
096        return getRequiredTokens();
097    }
098
099    /**
100     * @return allowed complexity.
101     */
102    public final int getMax() {
103        return max;
104    }
105
106    /**
107     * Sets maximum allowed complexity.
108     * @param max allowed complexity.
109     */
110    public final void setMax(int max) {
111        this.max = max;
112    }
113
114    /**
115     * Sets user-excluded classes to ignore.
116     * @param excludedClasses the list of classes to ignore.
117     */
118    public final void setExcludedClasses(String... excludedClasses) {
119        this.excludedClasses =
120            Collections.unmodifiableSet(Arrays.stream(excludedClasses).collect(Collectors.toSet()));
121    }
122
123    @Override
124    public final void beginTree(DetailAST ast) {
125        packageName = "";
126    }
127
128    @Override
129    public void visitToken(DetailAST ast) {
130        switch (ast.getType()) {
131            case TokenTypes.PACKAGE_DEF:
132                visitPackageDef(ast);
133                break;
134            case TokenTypes.CLASS_DEF:
135            case TokenTypes.INTERFACE_DEF:
136            case TokenTypes.ANNOTATION_DEF:
137            case TokenTypes.ENUM_DEF:
138                visitClassDef(ast);
139                break;
140            case TokenTypes.TYPE:
141                context.visitType(ast);
142                break;
143            case TokenTypes.LITERAL_NEW:
144                context.visitLiteralNew(ast);
145                break;
146            case TokenTypes.LITERAL_THROWS:
147                context.visitLiteralThrows(ast);
148                break;
149            default:
150                throw new IllegalArgumentException("Unknown type: " + ast);
151        }
152    }
153
154    @Override
155    public void leaveToken(DetailAST ast) {
156        switch (ast.getType()) {
157            case TokenTypes.CLASS_DEF:
158            case TokenTypes.INTERFACE_DEF:
159            case TokenTypes.ANNOTATION_DEF:
160            case TokenTypes.ENUM_DEF:
161                leaveClassDef();
162                break;
163            default:
164                // Do nothing
165        }
166    }
167
168    /**
169     * Stores package of current class we check.
170     * @param pkg package definition.
171     */
172    private void visitPackageDef(DetailAST pkg) {
173        final FullIdent ident = FullIdent.createFullIdent(pkg.getLastChild()
174                .getPreviousSibling());
175        packageName = ident.getText();
176    }
177
178    /**
179     * Creates new context for a given class.
180     * @param classDef class definition node.
181     */
182    private void visitClassDef(DetailAST classDef) {
183        contextStack.push(context);
184        final String className =
185            classDef.findFirstToken(TokenTypes.IDENT).getText();
186        context = new Context(className,
187                               classDef.getLineNo(),
188                               classDef.getColumnNo());
189    }
190
191    /** Restores previous context. */
192    private void leaveClassDef() {
193        context.checkCoupling();
194        context = contextStack.pop();
195    }
196
197    /**
198     * Encapsulates information about class coupling.
199     *
200     * @author <a href="mailto:simon@redhillconsulting.com.au">Simon Harris</a>
201     * @author o_sukhodolsky
202     */
203    private class Context {
204        /**
205         * Set of referenced classes.
206         * Sorted by name for predictable error messages in unit tests.
207         */
208        private final Set<String> referencedClassNames = new TreeSet<>();
209        /** Own class name. */
210        private final String className;
211        /* Location of own class. (Used to log violations) */
212        /** Line number of class definition. */
213        private final int lineNo;
214        /** Column number of class definition. */
215        private final int columnNo;
216
217        /**
218         * Create new context associated with given class.
219         * @param className name of the given class.
220         * @param lineNo line of class definition.
221         * @param columnNo column of class definition.
222         */
223        Context(String className, int lineNo, int columnNo) {
224            this.className = className;
225            this.lineNo = lineNo;
226            this.columnNo = columnNo;
227        }
228
229        /**
230         * Visits throws clause and collects all exceptions we throw.
231         * @param literalThrows throws to process.
232         */
233        public void visitLiteralThrows(DetailAST literalThrows) {
234            for (DetailAST childAST = literalThrows.getFirstChild();
235                 childAST != null;
236                 childAST = childAST.getNextSibling()) {
237                if (childAST.getType() != TokenTypes.COMMA) {
238                    addReferencedClassName(childAST);
239                }
240            }
241        }
242
243        /**
244         * Visits type.
245         * @param ast type to process.
246         */
247        public void visitType(DetailAST ast) {
248            final String fullTypeName = CheckUtils.createFullType(ast).getText();
249            context.addReferencedClassName(fullTypeName);
250        }
251
252        /**
253         * Visits NEW.
254         * @param ast NEW to process.
255         */
256        public void visitLiteralNew(DetailAST ast) {
257            context.addReferencedClassName(ast.getFirstChild());
258        }
259
260        /**
261         * Adds new referenced class.
262         * @param ast a node which represents referenced class.
263         */
264        private void addReferencedClassName(DetailAST ast) {
265            final String fullIdentName = FullIdent.createFullIdent(ast).getText();
266            addReferencedClassName(fullIdentName);
267        }
268
269        /**
270         * Adds new referenced class.
271         * @param referencedClassName class name of the referenced class.
272         */
273        private void addReferencedClassName(String referencedClassName) {
274            if (isSignificant(referencedClassName)) {
275                referencedClassNames.add(referencedClassName);
276            }
277        }
278
279        /** Checks if coupling less than allowed or not. */
280        public void checkCoupling() {
281            referencedClassNames.remove(className);
282            referencedClassNames.remove(packageName + "." + className);
283
284            if (referencedClassNames.size() > max) {
285                log(lineNo, columnNo, getLogMessageId(),
286                        referencedClassNames.size(), getMax(),
287                        referencedClassNames.toString());
288            }
289        }
290
291        /**
292         * Checks if given class shouldn't be ignored and not from java.lang.
293         * @param candidateClassName class to check.
294         * @return true if we should count this class.
295         */
296        private boolean isSignificant(String candidateClassName) {
297            return !excludedClasses.contains(candidateClassName)
298                    && !candidateClassName.startsWith("java.lang.");
299        }
300    }
301}