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 * Type is {@code int}.
060 * Default value is {@code 50}.
061 * </li>
062 * <li>
063 * Property {@code classMaximum} - Specify the maximum allowed number of
064 * non commenting lines in a class.
065 * Type is {@code int}.
066 * Default value is {@code 1500}.
067 * </li>
068 * <li>
069 * Property {@code fileMaximum} - Specify the maximum allowed number of
070 * non commenting lines in a file including all top level and nested classes.
071 * Type is {@code int}.
072 * Default value is {@code 2000}.
073 * </li>
074 * </ul>
075 * <p>
076 * To configure the check:
077 * </p>
078 * <pre>
079 * &lt;module name="JavaNCSS"/&gt;
080 * </pre>
081 * <p>Example:</p>
082 * <pre>
083 * public void test() {
084 *   System.out.println("Line 1");
085 *   // another 48 lines of code
086 *   System.out.println("Line 50") // OK
087 *   System.out.println("Line 51") // violation, the method crosses 50 non commented lines
088 * }
089 * </pre>
090 * <p>
091 * To configure the check with 40 allowed non commented lines for a method:
092 * </p>
093 * <pre>
094 * &lt;module name="JavaNCSS"&gt;
095 *   &lt;property name="methodMaximum" value="40"/&gt;
096 * &lt;/module&gt;
097 * </pre>
098 * <p>Example:</p>
099 * <pre>
100 * public void test() {
101 *   System.out.println("Line 1");
102 *   // another 38 lines of code
103 *   System.out.println("Line 40") // OK
104 *   System.out.println("Line 41") // violation, the method crosses 40 non commented lines
105 * }
106 * </pre>
107 * <p>
108 * To configure the check to set limit of non commented lines in class to 100:
109 * </p>
110 * <pre>
111 * &lt;module name="JavaNCSS"&gt;
112 *   &lt;property name="classMaximum" value="100"/&gt;
113 * &lt;/module&gt;
114 * </pre>
115 * <p>Example:</p>
116 * <pre>
117 * public class Test {
118 *   public void test() {
119 *       System.out.println("Line 1");
120 *       // another 47 lines of code
121 *       System.out.println("Line 49");
122 *   }
123 *
124 *   public void test1() {
125 *       System.out.println("Line 50"); // OK
126 *       // another 47 lines of code
127 *       System.out.println("Line 98"); // violation
128 *   }
129 * }
130 * </pre>
131 * <p>
132 * To configure the check to set limit of non commented lines in file to 200:
133 * </p>
134 * <pre>
135 * &lt;module name="JavaNCSS"&gt;
136 *   &lt;property name="fileMaximum" value="200"/&gt;
137 * &lt;/module&gt;
138 * </pre>
139 * <p>Example:</p>
140 * <pre>
141 * public class Test1 {
142 *   public void test() {
143 *       System.out.println("Line 1");
144 *       // another 48 lines of code
145 *       System.out.println("Line 49");
146 *   }
147 *
148 *   public void test1() {
149 *       System.out.println("Line 50");
150 *       // another 47 lines of code
151 *       System.out.println("Line 98"); // OK
152 *   }
153 * }
154 *
155 * class Test2 {
156 *   public void test() {
157 *       System.out.println("Line 150"); // OK
158 *   }
159 *
160 *   public void test1() {
161 *       System.out.println("Line 200"); // violation
162 *   }
163 * }
164 * </pre>
165 * <p>
166 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
167 * </p>
168 * <p>
169 * Violation Message Keys:
170 * </p>
171 * <ul>
172 * <li>
173 * {@code ncss.class}
174 * </li>
175 * <li>
176 * {@code ncss.file}
177 * </li>
178 * <li>
179 * {@code ncss.method}
180 * </li>
181 * </ul>
182 *
183 * @since 3.5
184 */
185// -@cs[AbbreviationAsWordInName] We can not change it as,
186// check's name is a part of API (used in configurations).
187@FileStatefulCheck
188public class JavaNCSSCheck extends AbstractCheck {
189
190    /**
191     * A key is pointing to the warning message text in "messages.properties"
192     * file.
193     */
194    public static final String MSG_METHOD = "ncss.method";
195
196    /**
197     * A key is pointing to the warning message text in "messages.properties"
198     * file.
199     */
200    public static final String MSG_CLASS = "ncss.class";
201
202    /**
203     * A key is pointing to the warning message text in "messages.properties"
204     * file.
205     */
206    public static final String MSG_FILE = "ncss.file";
207
208    /** Default constant for max file ncss. */
209    private static final int FILE_MAX_NCSS = 2000;
210
211    /** Default constant for max file ncss. */
212    private static final int CLASS_MAX_NCSS = 1500;
213
214    /** Default constant for max method ncss. */
215    private static final int METHOD_MAX_NCSS = 50;
216
217    /**
218     * Specify the maximum allowed number of non commenting lines in a file
219     * including all top level and nested classes.
220     */
221    private int fileMaximum = FILE_MAX_NCSS;
222
223    /** Specify the maximum allowed number of non commenting lines in a class. */
224    private int classMaximum = CLASS_MAX_NCSS;
225
226    /** Specify the maximum allowed number of non commenting lines in a method. */
227    private int methodMaximum = METHOD_MAX_NCSS;
228
229    /** List containing the stacked counters. */
230    private Deque<Counter> counters;
231
232    @Override
233    public int[] getDefaultTokens() {
234        return getRequiredTokens();
235    }
236
237    @Override
238    public int[] getRequiredTokens() {
239        return new int[] {
240            TokenTypes.CLASS_DEF,
241            TokenTypes.INTERFACE_DEF,
242            TokenTypes.METHOD_DEF,
243            TokenTypes.CTOR_DEF,
244            TokenTypes.INSTANCE_INIT,
245            TokenTypes.STATIC_INIT,
246            TokenTypes.PACKAGE_DEF,
247            TokenTypes.IMPORT,
248            TokenTypes.VARIABLE_DEF,
249            TokenTypes.CTOR_CALL,
250            TokenTypes.SUPER_CTOR_CALL,
251            TokenTypes.LITERAL_IF,
252            TokenTypes.LITERAL_ELSE,
253            TokenTypes.LITERAL_WHILE,
254            TokenTypes.LITERAL_DO,
255            TokenTypes.LITERAL_FOR,
256            TokenTypes.LITERAL_SWITCH,
257            TokenTypes.LITERAL_BREAK,
258            TokenTypes.LITERAL_CONTINUE,
259            TokenTypes.LITERAL_RETURN,
260            TokenTypes.LITERAL_THROW,
261            TokenTypes.LITERAL_SYNCHRONIZED,
262            TokenTypes.LITERAL_CATCH,
263            TokenTypes.LITERAL_FINALLY,
264            TokenTypes.EXPR,
265            TokenTypes.LABELED_STAT,
266            TokenTypes.LITERAL_CASE,
267            TokenTypes.LITERAL_DEFAULT,
268        };
269    }
270
271    @Override
272    public int[] getAcceptableTokens() {
273        return getRequiredTokens();
274    }
275
276    @Override
277    public void beginTree(DetailAST rootAST) {
278        counters = new ArrayDeque<>();
279
280        // add a counter for the file
281        counters.push(new Counter());
282    }
283
284    @Override
285    public void visitToken(DetailAST ast) {
286        final int tokenType = ast.getType();
287
288        if (tokenType == TokenTypes.CLASS_DEF
289            || tokenType == TokenTypes.METHOD_DEF
290            || tokenType == TokenTypes.CTOR_DEF
291            || tokenType == TokenTypes.STATIC_INIT
292            || tokenType == TokenTypes.INSTANCE_INIT) {
293            // add a counter for this class/method
294            counters.push(new Counter());
295        }
296
297        // check if token is countable
298        if (isCountable(ast)) {
299            // increment the stacked counters
300            counters.forEach(Counter::increment);
301        }
302    }
303
304    @Override
305    public void leaveToken(DetailAST ast) {
306        final int tokenType = ast.getType();
307        if (tokenType == TokenTypes.METHOD_DEF
308            || tokenType == TokenTypes.CTOR_DEF
309            || tokenType == TokenTypes.STATIC_INIT
310            || tokenType == TokenTypes.INSTANCE_INIT) {
311            // pop counter from the stack
312            final Counter counter = counters.pop();
313
314            final int count = counter.getCount();
315            if (count > methodMaximum) {
316                log(ast, MSG_METHOD, count, methodMaximum);
317            }
318        }
319        else if (tokenType == TokenTypes.CLASS_DEF) {
320            // pop counter from the stack
321            final Counter counter = counters.pop();
322
323            final int count = counter.getCount();
324            if (count > classMaximum) {
325                log(ast, MSG_CLASS, count, classMaximum);
326            }
327        }
328    }
329
330    @Override
331    public void finishTree(DetailAST rootAST) {
332        // pop counter from the stack
333        final Counter counter = counters.pop();
334
335        final int count = counter.getCount();
336        if (count > fileMaximum) {
337            log(rootAST, MSG_FILE, count, fileMaximum);
338        }
339    }
340
341    /**
342     * Setter to specify the maximum allowed number of non commenting lines
343     * in a file including all top level and nested classes.
344     *
345     * @param fileMaximum
346     *            the maximum ncss
347     */
348    public void setFileMaximum(int fileMaximum) {
349        this.fileMaximum = fileMaximum;
350    }
351
352    /**
353     * Setter to specify the maximum allowed number of non commenting lines in a class.
354     *
355     * @param classMaximum
356     *            the maximum ncss
357     */
358    public void setClassMaximum(int classMaximum) {
359        this.classMaximum = classMaximum;
360    }
361
362    /**
363     * Setter to specify the maximum allowed number of non commenting lines in a method.
364     *
365     * @param methodMaximum
366     *            the maximum ncss
367     */
368    public void setMethodMaximum(int methodMaximum) {
369        this.methodMaximum = methodMaximum;
370    }
371
372    /**
373     * Checks if a token is countable for the ncss metric.
374     *
375     * @param ast
376     *            the AST
377     * @return true if the token is countable
378     */
379    private static boolean isCountable(DetailAST ast) {
380        boolean countable = true;
381
382        final int tokenType = ast.getType();
383
384        // check if an expression is countable
385        if (tokenType == TokenTypes.EXPR) {
386            countable = isExpressionCountable(ast);
387        }
388        // check if an variable definition is countable
389        else if (tokenType == TokenTypes.VARIABLE_DEF) {
390            countable = isVariableDefCountable(ast);
391        }
392        return countable;
393    }
394
395    /**
396     * Checks if a variable definition is countable.
397     *
398     * @param ast the AST
399     * @return true if the variable definition is countable, false otherwise
400     */
401    private static boolean isVariableDefCountable(DetailAST ast) {
402        boolean countable = false;
403
404        // count variable definitions only if they are direct child to a slist or
405        // object block
406        final int parentType = ast.getParent().getType();
407
408        if (parentType == TokenTypes.SLIST
409            || parentType == TokenTypes.OBJBLOCK) {
410            final DetailAST prevSibling = ast.getPreviousSibling();
411
412            // is countable if no previous sibling is found or
413            // the sibling is no COMMA.
414            // This is done because multiple assignment on one line are counted
415            // as 1
416            countable = prevSibling == null
417                    || prevSibling.getType() != TokenTypes.COMMA;
418        }
419
420        return countable;
421    }
422
423    /**
424     * Checks if an expression is countable for the ncss metric.
425     *
426     * @param ast the AST
427     * @return true if the expression is countable, false otherwise
428     */
429    private static boolean isExpressionCountable(DetailAST ast) {
430        final boolean countable;
431
432        // count expressions only if they are direct child to a slist (method
433        // body, for loop...)
434        // or direct child of label,if,else,do,while,for
435        final int parentType = ast.getParent().getType();
436        switch (parentType) {
437            case TokenTypes.SLIST:
438            case TokenTypes.LABELED_STAT:
439            case TokenTypes.LITERAL_FOR:
440            case TokenTypes.LITERAL_DO:
441            case TokenTypes.LITERAL_WHILE:
442            case TokenTypes.LITERAL_IF:
443            case TokenTypes.LITERAL_ELSE:
444                // don't count if or loop conditions
445                final DetailAST prevSibling = ast.getPreviousSibling();
446                countable = prevSibling == null
447                    || prevSibling.getType() != TokenTypes.LPAREN;
448                break;
449            default:
450                countable = false;
451                break;
452        }
453        return countable;
454    }
455
456    /**
457     * Class representing a counter.
458     *
459     */
460    private static class Counter {
461
462        /** The counters internal integer. */
463        private int count;
464
465        /**
466         * Increments the counter.
467         */
468        public void increment() {
469            count++;
470        }
471
472        /**
473         * Gets the counters value.
474         *
475         * @return the counter
476         */
477        public int getCount() {
478            return count;
479        }
480
481    }
482
483}