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