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.Deque;
024
025import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
026import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
027import com.puppycrawl.tools.checkstyle.api.DetailAST;
028import com.puppycrawl.tools.checkstyle.api.TokenTypes;
029
030/**
031 * This check calculates the Non Commenting Source Statements (NCSS) metric for
032 * java source files and methods. The check adheres to the <a
033 * href="http://www.kclee.com/clemens/java/javancss">JavaNCSS specification
034 * </a> and gives the same results as the JavaNCSS tool.
035 *
036 * <p>The NCSS-metric tries to determine complexity of methods, classes and files
037 * by counting the non commenting lines. Roughly said this is (nearly)
038 * equivalent to counting the semicolons and opening curly braces.
039 *
040 */
041// -@cs[AbbreviationAsWordInName] We can not change it as,
042// check's name is a part of API (used in configurations).
043@FileStatefulCheck
044public class JavaNCSSCheck extends AbstractCheck {
045
046    /**
047     * A key is pointing to the warning message text in "messages.properties"
048     * file.
049     */
050    public static final String MSG_METHOD = "ncss.method";
051
052    /**
053     * A key is pointing to the warning message text in "messages.properties"
054     * file.
055     */
056    public static final String MSG_CLASS = "ncss.class";
057
058    /**
059     * A key is pointing to the warning message text in "messages.properties"
060     * file.
061     */
062    public static final String MSG_FILE = "ncss.file";
063
064    /** Default constant for max file ncss. */
065    private static final int FILE_MAX_NCSS = 2000;
066
067    /** Default constant for max file ncss. */
068    private static final int CLASS_MAX_NCSS = 1500;
069
070    /** Default constant for max method ncss. */
071    private static final int METHOD_MAX_NCSS = 50;
072
073    /** Maximum ncss for a complete source file. */
074    private int fileMaximum = FILE_MAX_NCSS;
075
076    /** Maximum ncss for a class. */
077    private int classMaximum = CLASS_MAX_NCSS;
078
079    /** Maximum ncss for a method. */
080    private int methodMaximum = METHOD_MAX_NCSS;
081
082    /** List containing the stacked counters. */
083    private Deque<Counter> counters;
084
085    @Override
086    public int[] getDefaultTokens() {
087        return getRequiredTokens();
088    }
089
090    @Override
091    public int[] getRequiredTokens() {
092        return new int[] {
093            TokenTypes.CLASS_DEF,
094            TokenTypes.INTERFACE_DEF,
095            TokenTypes.METHOD_DEF,
096            TokenTypes.CTOR_DEF,
097            TokenTypes.INSTANCE_INIT,
098            TokenTypes.STATIC_INIT,
099            TokenTypes.PACKAGE_DEF,
100            TokenTypes.IMPORT,
101            TokenTypes.VARIABLE_DEF,
102            TokenTypes.CTOR_CALL,
103            TokenTypes.SUPER_CTOR_CALL,
104            TokenTypes.LITERAL_IF,
105            TokenTypes.LITERAL_ELSE,
106            TokenTypes.LITERAL_WHILE,
107            TokenTypes.LITERAL_DO,
108            TokenTypes.LITERAL_FOR,
109            TokenTypes.LITERAL_SWITCH,
110            TokenTypes.LITERAL_BREAK,
111            TokenTypes.LITERAL_CONTINUE,
112            TokenTypes.LITERAL_RETURN,
113            TokenTypes.LITERAL_THROW,
114            TokenTypes.LITERAL_SYNCHRONIZED,
115            TokenTypes.LITERAL_CATCH,
116            TokenTypes.LITERAL_FINALLY,
117            TokenTypes.EXPR,
118            TokenTypes.LABELED_STAT,
119            TokenTypes.LITERAL_CASE,
120            TokenTypes.LITERAL_DEFAULT,
121        };
122    }
123
124    @Override
125    public int[] getAcceptableTokens() {
126        return getRequiredTokens();
127    }
128
129    @Override
130    public void beginTree(DetailAST rootAST) {
131        counters = new ArrayDeque<>();
132
133        //add a counter for the file
134        counters.push(new Counter());
135    }
136
137    @Override
138    public void visitToken(DetailAST ast) {
139        final int tokenType = ast.getType();
140
141        if (tokenType == TokenTypes.CLASS_DEF
142            || tokenType == TokenTypes.METHOD_DEF
143            || tokenType == TokenTypes.CTOR_DEF
144            || tokenType == TokenTypes.STATIC_INIT
145            || tokenType == TokenTypes.INSTANCE_INIT) {
146            //add a counter for this class/method
147            counters.push(new Counter());
148        }
149
150        //check if token is countable
151        if (isCountable(ast)) {
152            //increment the stacked counters
153            counters.forEach(Counter::increment);
154        }
155    }
156
157    @Override
158    public void leaveToken(DetailAST ast) {
159        final int tokenType = ast.getType();
160        if (tokenType == TokenTypes.METHOD_DEF
161            || tokenType == TokenTypes.CTOR_DEF
162            || tokenType == TokenTypes.STATIC_INIT
163            || tokenType == TokenTypes.INSTANCE_INIT) {
164            //pop counter from the stack
165            final Counter counter = counters.pop();
166
167            final int count = counter.getCount();
168            if (count > methodMaximum) {
169                log(ast.getLineNo(), ast.getColumnNo(), MSG_METHOD,
170                        count, methodMaximum);
171            }
172        }
173        else if (tokenType == TokenTypes.CLASS_DEF) {
174            //pop counter from the stack
175            final Counter counter = counters.pop();
176
177            final int count = counter.getCount();
178            if (count > classMaximum) {
179                log(ast.getLineNo(), ast.getColumnNo(), MSG_CLASS,
180                        count, classMaximum);
181            }
182        }
183    }
184
185    @Override
186    public void finishTree(DetailAST rootAST) {
187        //pop counter from the stack
188        final Counter counter = counters.pop();
189
190        final int count = counter.getCount();
191        if (count > fileMaximum) {
192            log(rootAST.getLineNo(), rootAST.getColumnNo(), MSG_FILE,
193                    count, fileMaximum);
194        }
195    }
196
197    /**
198     * Sets the maximum ncss for a file.
199     *
200     * @param fileMaximum
201     *            the maximum ncss
202     */
203    public void setFileMaximum(int fileMaximum) {
204        this.fileMaximum = fileMaximum;
205    }
206
207    /**
208     * Sets the maximum ncss for a class.
209     *
210     * @param classMaximum
211     *            the maximum ncss
212     */
213    public void setClassMaximum(int classMaximum) {
214        this.classMaximum = classMaximum;
215    }
216
217    /**
218     * Sets the maximum ncss for a method.
219     *
220     * @param methodMaximum
221     *            the maximum ncss
222     */
223    public void setMethodMaximum(int methodMaximum) {
224        this.methodMaximum = methodMaximum;
225    }
226
227    /**
228     * Checks if a token is countable for the ncss metric.
229     *
230     * @param ast
231     *            the AST
232     * @return true if the token is countable
233     */
234    private static boolean isCountable(DetailAST ast) {
235        boolean countable = true;
236
237        final int tokenType = ast.getType();
238
239        //check if an expression is countable
240        if (tokenType == TokenTypes.EXPR) {
241            countable = isExpressionCountable(ast);
242        }
243        //check if an variable definition is countable
244        else if (tokenType == TokenTypes.VARIABLE_DEF) {
245            countable = isVariableDefCountable(ast);
246        }
247        return countable;
248    }
249
250    /**
251     * Checks if a variable definition is countable.
252     *
253     * @param ast the AST
254     * @return true if the variable definition is countable, false otherwise
255     */
256    private static boolean isVariableDefCountable(DetailAST ast) {
257        boolean countable = false;
258
259        //count variable definitions only if they are direct child to a slist or
260        // object block
261        final int parentType = ast.getParent().getType();
262
263        if (parentType == TokenTypes.SLIST
264            || parentType == TokenTypes.OBJBLOCK) {
265            final DetailAST prevSibling = ast.getPreviousSibling();
266
267            //is countable if no previous sibling is found or
268            //the sibling is no COMMA.
269            //This is done because multiple assignment on one line are counted
270            // as 1
271            countable = prevSibling == null
272                    || prevSibling.getType() != TokenTypes.COMMA;
273        }
274
275        return countable;
276    }
277
278    /**
279     * Checks if an expression is countable for the ncss metric.
280     *
281     * @param ast the AST
282     * @return true if the expression is countable, false otherwise
283     */
284    private static boolean isExpressionCountable(DetailAST ast) {
285        final boolean countable;
286
287        //count expressions only if they are direct child to a slist (method
288        // body, for loop...)
289        //or direct child of label,if,else,do,while,for
290        final int parentType = ast.getParent().getType();
291        switch (parentType) {
292            case TokenTypes.SLIST :
293            case TokenTypes.LABELED_STAT :
294            case TokenTypes.LITERAL_FOR :
295            case TokenTypes.LITERAL_DO :
296            case TokenTypes.LITERAL_WHILE :
297            case TokenTypes.LITERAL_IF :
298            case TokenTypes.LITERAL_ELSE :
299                //don't count if or loop conditions
300                final DetailAST prevSibling = ast.getPreviousSibling();
301                countable = prevSibling == null
302                    || prevSibling.getType() != TokenTypes.LPAREN;
303                break;
304            default :
305                countable = false;
306                break;
307        }
308        return countable;
309    }
310
311    /**
312     * Class representing a counter.
313     *
314     */
315    private static class Counter {
316
317        /** The counters internal integer. */
318        private int count;
319
320        /**
321         * Increments the counter.
322         */
323        public void increment() {
324            count++;
325        }
326
327        /**
328         * Gets the counters value.
329         *
330         * @return the counter
331         */
332        public int getCount() {
333            return count;
334        }
335
336    }
337
338}