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