001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2020 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 * <p>
032 * Determines complexity of methods, classes and files by counting
033 * the Non Commenting Source Statements (NCSS). This check adheres to the
034 * <a href="http://www.kclee.de/clemens/java/javancss/#specification">specification</a>
035 * for the <a href="http://www.kclee.de/clemens/java/javancss/">JavaNCSS-Tool</a>
036 * written by <b>Chr. Clemens Lee</b>.
037 * </p>
038 * <p>
039 * Roughly said the NCSS metric is calculated by counting the source lines which are
040 * not comments, (nearly) equivalent to counting the semicolons and opening curly braces.
041 * </p>
042 * <p>
043 * The NCSS for a class is summarized from the NCSS of all its methods, the NCSS
044 * of its nested classes and the number of member variable declarations.
045 * </p>
046 * <p>
047 * The NCSS for a file is summarized from the ncss of all its top level classes,
048 * the number of imports and the package declaration.
049 * </p>
050 * <p>
051 * Rationale: Too large methods and classes are hard to read and costly to maintain.
052 * A large NCSS number often means that a method or class has too many responsibilities
053 * and/or functionalities which should be decomposed into smaller units.
054 * </p>
055 * <ul>
056 * <li>
057 * Property {@code methodMaximum} - Specify the maximum allowed number of
058 * non commenting lines in a method.
059 * Default value is {@code 50}.
060 * </li>
061 * <li>
062 * Property {@code classMaximum} - Specify the maximum allowed number of
063 * non commenting lines in a class.
064 * Default value is {@code 1500}.
065 * </li>
066 * <li>
067 * Property {@code fileMaximum} - Specify the maximum allowed number of
068 * non commenting lines in a file including all top level and nested classes.
069 * Default value is {@code 2000}.
070 * </li>
071 * </ul>
072 * <p>
073 * To configure the check:
074 * </p>
075 * <pre>
076 * &lt;module name="JavaNCSS"/&gt;
077 * </pre>
078 * <p>
079 * To configure the check with 40 allowed non commenting lines for a method:
080 * </p>
081 * <pre>
082 * &lt;module name="JavaNCSS"&gt;
083 *   &lt;property name="methodMaximum" value="40"/&gt;
084 * &lt;/module&gt;
085 * </pre>
086 *
087 * @since 3.5
088 */
089// -@cs[AbbreviationAsWordInName] We can not change it as,
090// check's name is a part of API (used in configurations).
091@FileStatefulCheck
092public class JavaNCSSCheck extends AbstractCheck {
093
094    /**
095     * A key is pointing to the warning message text in "messages.properties"
096     * file.
097     */
098    public static final String MSG_METHOD = "ncss.method";
099
100    /**
101     * A key is pointing to the warning message text in "messages.properties"
102     * file.
103     */
104    public static final String MSG_CLASS = "ncss.class";
105
106    /**
107     * A key is pointing to the warning message text in "messages.properties"
108     * file.
109     */
110    public static final String MSG_FILE = "ncss.file";
111
112    /** Default constant for max file ncss. */
113    private static final int FILE_MAX_NCSS = 2000;
114
115    /** Default constant for max file ncss. */
116    private static final int CLASS_MAX_NCSS = 1500;
117
118    /** Default constant for max method ncss. */
119    private static final int METHOD_MAX_NCSS = 50;
120
121    /**
122     * Specify the maximum allowed number of non commenting lines in a file
123     * including all top level and nested classes.
124     */
125    private int fileMaximum = FILE_MAX_NCSS;
126
127    /** Specify the maximum allowed number of non commenting lines in a class. */
128    private int classMaximum = CLASS_MAX_NCSS;
129
130    /** Specify the maximum allowed number of non commenting lines in a method. */
131    private int methodMaximum = METHOD_MAX_NCSS;
132
133    /** List containing the stacked counters. */
134    private Deque<Counter> counters;
135
136    @Override
137    public int[] getDefaultTokens() {
138        return getRequiredTokens();
139    }
140
141    @Override
142    public int[] getRequiredTokens() {
143        return new int[] {
144            TokenTypes.CLASS_DEF,
145            TokenTypes.INTERFACE_DEF,
146            TokenTypes.METHOD_DEF,
147            TokenTypes.CTOR_DEF,
148            TokenTypes.INSTANCE_INIT,
149            TokenTypes.STATIC_INIT,
150            TokenTypes.PACKAGE_DEF,
151            TokenTypes.IMPORT,
152            TokenTypes.VARIABLE_DEF,
153            TokenTypes.CTOR_CALL,
154            TokenTypes.SUPER_CTOR_CALL,
155            TokenTypes.LITERAL_IF,
156            TokenTypes.LITERAL_ELSE,
157            TokenTypes.LITERAL_WHILE,
158            TokenTypes.LITERAL_DO,
159            TokenTypes.LITERAL_FOR,
160            TokenTypes.LITERAL_SWITCH,
161            TokenTypes.LITERAL_BREAK,
162            TokenTypes.LITERAL_CONTINUE,
163            TokenTypes.LITERAL_RETURN,
164            TokenTypes.LITERAL_THROW,
165            TokenTypes.LITERAL_SYNCHRONIZED,
166            TokenTypes.LITERAL_CATCH,
167            TokenTypes.LITERAL_FINALLY,
168            TokenTypes.EXPR,
169            TokenTypes.LABELED_STAT,
170            TokenTypes.LITERAL_CASE,
171            TokenTypes.LITERAL_DEFAULT,
172        };
173    }
174
175    @Override
176    public int[] getAcceptableTokens() {
177        return getRequiredTokens();
178    }
179
180    @Override
181    public void beginTree(DetailAST rootAST) {
182        counters = new ArrayDeque<>();
183
184        //add a counter for the file
185        counters.push(new Counter());
186    }
187
188    @Override
189    public void visitToken(DetailAST ast) {
190        final int tokenType = ast.getType();
191
192        if (tokenType == TokenTypes.CLASS_DEF
193            || tokenType == TokenTypes.METHOD_DEF
194            || tokenType == TokenTypes.CTOR_DEF
195            || tokenType == TokenTypes.STATIC_INIT
196            || tokenType == TokenTypes.INSTANCE_INIT) {
197            //add a counter for this class/method
198            counters.push(new Counter());
199        }
200
201        //check if token is countable
202        if (isCountable(ast)) {
203            //increment the stacked counters
204            counters.forEach(Counter::increment);
205        }
206    }
207
208    @Override
209    public void leaveToken(DetailAST ast) {
210        final int tokenType = ast.getType();
211        if (tokenType == TokenTypes.METHOD_DEF
212            || tokenType == TokenTypes.CTOR_DEF
213            || tokenType == TokenTypes.STATIC_INIT
214            || tokenType == TokenTypes.INSTANCE_INIT) {
215            //pop counter from the stack
216            final Counter counter = counters.pop();
217
218            final int count = counter.getCount();
219            if (count > methodMaximum) {
220                log(ast, MSG_METHOD, count, methodMaximum);
221            }
222        }
223        else if (tokenType == TokenTypes.CLASS_DEF) {
224            //pop counter from the stack
225            final Counter counter = counters.pop();
226
227            final int count = counter.getCount();
228            if (count > classMaximum) {
229                log(ast, MSG_CLASS, count, classMaximum);
230            }
231        }
232    }
233
234    @Override
235    public void finishTree(DetailAST rootAST) {
236        //pop counter from the stack
237        final Counter counter = counters.pop();
238
239        final int count = counter.getCount();
240        if (count > fileMaximum) {
241            log(rootAST, MSG_FILE, count, fileMaximum);
242        }
243    }
244
245    /**
246     * Setter to specify the maximum allowed number of non commenting lines
247     * in a file including all top level and nested classes.
248     *
249     * @param fileMaximum
250     *            the maximum ncss
251     */
252    public void setFileMaximum(int fileMaximum) {
253        this.fileMaximum = fileMaximum;
254    }
255
256    /**
257     * Setter to specify the maximum allowed number of non commenting lines in a class.
258     *
259     * @param classMaximum
260     *            the maximum ncss
261     */
262    public void setClassMaximum(int classMaximum) {
263        this.classMaximum = classMaximum;
264    }
265
266    /**
267     * Setter to specify the maximum allowed number of non commenting lines in a method.
268     *
269     * @param methodMaximum
270     *            the maximum ncss
271     */
272    public void setMethodMaximum(int methodMaximum) {
273        this.methodMaximum = methodMaximum;
274    }
275
276    /**
277     * Checks if a token is countable for the ncss metric.
278     *
279     * @param ast
280     *            the AST
281     * @return true if the token is countable
282     */
283    private static boolean isCountable(DetailAST ast) {
284        boolean countable = true;
285
286        final int tokenType = ast.getType();
287
288        //check if an expression is countable
289        if (tokenType == TokenTypes.EXPR) {
290            countable = isExpressionCountable(ast);
291        }
292        //check if an variable definition is countable
293        else if (tokenType == TokenTypes.VARIABLE_DEF) {
294            countable = isVariableDefCountable(ast);
295        }
296        return countable;
297    }
298
299    /**
300     * Checks if a variable definition is countable.
301     *
302     * @param ast the AST
303     * @return true if the variable definition is countable, false otherwise
304     */
305    private static boolean isVariableDefCountable(DetailAST ast) {
306        boolean countable = false;
307
308        //count variable definitions only if they are direct child to a slist or
309        // object block
310        final int parentType = ast.getParent().getType();
311
312        if (parentType == TokenTypes.SLIST
313            || parentType == TokenTypes.OBJBLOCK) {
314            final DetailAST prevSibling = ast.getPreviousSibling();
315
316            //is countable if no previous sibling is found or
317            //the sibling is no COMMA.
318            //This is done because multiple assignment on one line are counted
319            // as 1
320            countable = prevSibling == null
321                    || prevSibling.getType() != TokenTypes.COMMA;
322        }
323
324        return countable;
325    }
326
327    /**
328     * Checks if an expression is countable for the ncss metric.
329     *
330     * @param ast the AST
331     * @return true if the expression is countable, false otherwise
332     */
333    private static boolean isExpressionCountable(DetailAST ast) {
334        final boolean countable;
335
336        //count expressions only if they are direct child to a slist (method
337        // body, for loop...)
338        //or direct child of label,if,else,do,while,for
339        final int parentType = ast.getParent().getType();
340        switch (parentType) {
341            case TokenTypes.SLIST :
342            case TokenTypes.LABELED_STAT :
343            case TokenTypes.LITERAL_FOR :
344            case TokenTypes.LITERAL_DO :
345            case TokenTypes.LITERAL_WHILE :
346            case TokenTypes.LITERAL_IF :
347            case TokenTypes.LITERAL_ELSE :
348                //don't count if or loop conditions
349                final DetailAST prevSibling = ast.getPreviousSibling();
350                countable = prevSibling == null
351                    || prevSibling.getType() != TokenTypes.LPAREN;
352                break;
353            default :
354                countable = false;
355                break;
356        }
357        return countable;
358    }
359
360    /**
361     * Class representing a counter.
362     *
363     */
364    private static class Counter {
365
366        /** The counters internal integer. */
367        private int count;
368
369        /**
370         * Increments the counter.
371         */
372        public void increment() {
373            count++;
374        }
375
376        /**
377         * Gets the counters value.
378         *
379         * @return the counter
380         */
381        public int getCount() {
382            return count;
383        }
384
385    }
386
387}