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