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