001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2019 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, MSG_METHOD, count, methodMaximum);
170            }
171        }
172        else if (tokenType == TokenTypes.CLASS_DEF) {
173            //pop counter from the stack
174            final Counter counter = counters.pop();
175
176            final int count = counter.getCount();
177            if (count > classMaximum) {
178                log(ast, MSG_CLASS, count, classMaximum);
179            }
180        }
181    }
182
183    @Override
184    public void finishTree(DetailAST rootAST) {
185        //pop counter from the stack
186        final Counter counter = counters.pop();
187
188        final int count = counter.getCount();
189        if (count > fileMaximum) {
190            log(rootAST, MSG_FILE, count, fileMaximum);
191        }
192    }
193
194    /**
195     * Sets the maximum ncss for a file.
196     *
197     * @param fileMaximum
198     *            the maximum ncss
199     */
200    public void setFileMaximum(int fileMaximum) {
201        this.fileMaximum = fileMaximum;
202    }
203
204    /**
205     * Sets the maximum ncss for a class.
206     *
207     * @param classMaximum
208     *            the maximum ncss
209     */
210    public void setClassMaximum(int classMaximum) {
211        this.classMaximum = classMaximum;
212    }
213
214    /**
215     * Sets the maximum ncss for a method.
216     *
217     * @param methodMaximum
218     *            the maximum ncss
219     */
220    public void setMethodMaximum(int methodMaximum) {
221        this.methodMaximum = methodMaximum;
222    }
223
224    /**
225     * Checks if a token is countable for the ncss metric.
226     *
227     * @param ast
228     *            the AST
229     * @return true if the token is countable
230     */
231    private static boolean isCountable(DetailAST ast) {
232        boolean countable = true;
233
234        final int tokenType = ast.getType();
235
236        //check if an expression is countable
237        if (tokenType == TokenTypes.EXPR) {
238            countable = isExpressionCountable(ast);
239        }
240        //check if an variable definition is countable
241        else if (tokenType == TokenTypes.VARIABLE_DEF) {
242            countable = isVariableDefCountable(ast);
243        }
244        return countable;
245    }
246
247    /**
248     * Checks if a variable definition is countable.
249     *
250     * @param ast the AST
251     * @return true if the variable definition is countable, false otherwise
252     */
253    private static boolean isVariableDefCountable(DetailAST ast) {
254        boolean countable = false;
255
256        //count variable definitions only if they are direct child to a slist or
257        // object block
258        final int parentType = ast.getParent().getType();
259
260        if (parentType == TokenTypes.SLIST
261            || parentType == TokenTypes.OBJBLOCK) {
262            final DetailAST prevSibling = ast.getPreviousSibling();
263
264            //is countable if no previous sibling is found or
265            //the sibling is no COMMA.
266            //This is done because multiple assignment on one line are counted
267            // as 1
268            countable = prevSibling == null
269                    || prevSibling.getType() != TokenTypes.COMMA;
270        }
271
272        return countable;
273    }
274
275    /**
276     * Checks if an expression is countable for the ncss metric.
277     *
278     * @param ast the AST
279     * @return true if the expression is countable, false otherwise
280     */
281    private static boolean isExpressionCountable(DetailAST ast) {
282        final boolean countable;
283
284        //count expressions only if they are direct child to a slist (method
285        // body, for loop...)
286        //or direct child of label,if,else,do,while,for
287        final int parentType = ast.getParent().getType();
288        switch (parentType) {
289            case TokenTypes.SLIST :
290            case TokenTypes.LABELED_STAT :
291            case TokenTypes.LITERAL_FOR :
292            case TokenTypes.LITERAL_DO :
293            case TokenTypes.LITERAL_WHILE :
294            case TokenTypes.LITERAL_IF :
295            case TokenTypes.LITERAL_ELSE :
296                //don't count if or loop conditions
297                final DetailAST prevSibling = ast.getPreviousSibling();
298                countable = prevSibling == null
299                    || prevSibling.getType() != TokenTypes.LPAREN;
300                break;
301            default :
302                countable = false;
303                break;
304        }
305        return countable;
306    }
307
308    /**
309     * Class representing a counter.
310     *
311     */
312    private static class Counter {
313
314        /** The counters internal integer. */
315        private int count;
316
317        /**
318         * Increments the counter.
319         */
320        public void increment() {
321            count++;
322        }
323
324        /**
325         * Gets the counters value.
326         *
327         * @return the counter
328         */
329        public int getCount() {
330            return count;
331        }
332
333    }
334
335}