View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.metrics;
21  
22  import java.util.ArrayDeque;
23  import java.util.Deque;
24  
25  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
26  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
27  import com.puppycrawl.tools.checkstyle.api.DetailAST;
28  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
29  
30  /**
31   * <div>
32   * Determines complexity of methods, classes and files by counting
33   * the Non Commenting Source Statements (NCSS). This check adheres to the
34   * <a href="http://www.kclee.de/clemens/java/javancss/#specification">specification</a>
35   * for the <a href="http://www.kclee.de/clemens/java/javancss/">JavaNCSS-Tool</a>
36   * written by <b>Chr. Clemens Lee</b>.
37   * </div>
38   *
39   * <p>
40   * Roughly said the NCSS metric is calculated by counting the source lines which are
41   * not comments, (nearly) equivalent to counting the semicolons and opening curly braces.
42   * </p>
43   *
44   * <p>
45   * The NCSS for a class is summarized from the NCSS of all its methods, the NCSS
46   * of its nested classes and the number of member variable declarations.
47   * </p>
48   *
49   * <p>
50   * The NCSS for a file is summarized from the ncss of all its top level classes,
51   * the number of imports and the package declaration.
52   * </p>
53   *
54   * <p>
55   * Rationale: Too large methods and classes are hard to read and costly to maintain.
56   * A large NCSS number often means that a method or class has too many responsibilities
57   * and/or functionalities which should be decomposed into smaller units.
58   * </p>
59   * <ul>
60   * <li>
61   * Property {@code classMaximum} - Specify the maximum allowed number of
62   * non commenting lines in a class.
63   * Type is {@code int}.
64   * Default value is {@code 1500}.
65   * </li>
66   * <li>
67   * Property {@code fileMaximum} - Specify the maximum allowed number of
68   * non commenting lines in a file including all top level and nested classes.
69   * Type is {@code int}.
70   * Default value is {@code 2000}.
71   * </li>
72   * <li>
73   * Property {@code methodMaximum} - Specify the maximum allowed number of
74   * non commenting lines in a method.
75   * Type is {@code int}.
76   * Default value is {@code 50}.
77   * </li>
78   * <li>
79   * Property {@code recordMaximum} - Specify the maximum allowed number of
80   * non commenting lines in a record.
81   * Type is {@code int}.
82   * Default value is {@code 150}.
83   * </li>
84   * </ul>
85   *
86   * <p>
87   * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
88   * </p>
89   *
90   * <p>
91   * Violation Message Keys:
92   * </p>
93   * <ul>
94   * <li>
95   * {@code ncss.class}
96   * </li>
97   * <li>
98   * {@code ncss.file}
99   * </li>
100  * <li>
101  * {@code ncss.method}
102  * </li>
103  * <li>
104  * {@code ncss.record}
105  * </li>
106  * </ul>
107  *
108  * @since 3.5
109  */
110 // -@cs[AbbreviationAsWordInName] We can not change it as,
111 // check's name is a part of API (used in configurations).
112 @FileStatefulCheck
113 public class JavaNCSSCheck extends AbstractCheck {
114 
115     /**
116      * A key is pointing to the warning message text in "messages.properties"
117      * file.
118      */
119     public static final String MSG_METHOD = "ncss.method";
120 
121     /**
122      * A key is pointing to the warning message text in "messages.properties"
123      * file.
124      */
125     public static final String MSG_CLASS = "ncss.class";
126 
127     /**
128      * A key is pointing to the warning message text in "messages.properties"
129      * file.
130      */
131     public static final String MSG_RECORD = "ncss.record";
132 
133     /**
134      * A key is pointing to the warning message text in "messages.properties"
135      * file.
136      */
137     public static final String MSG_FILE = "ncss.file";
138 
139     /** Default constant for max file ncss. */
140     private static final int FILE_MAX_NCSS = 2000;
141 
142     /** Default constant for max file ncss. */
143     private static final int CLASS_MAX_NCSS = 1500;
144 
145     /** Default constant for max record ncss. */
146     private static final int RECORD_MAX_NCSS = 150;
147 
148     /** Default constant for max method ncss. */
149     private static final int METHOD_MAX_NCSS = 50;
150 
151     /**
152      * Specify the maximum allowed number of non commenting lines in a file
153      * including all top level and nested classes.
154      */
155     private int fileMaximum = FILE_MAX_NCSS;
156 
157     /** Specify the maximum allowed number of non commenting lines in a class. */
158     private int classMaximum = CLASS_MAX_NCSS;
159 
160     /** Specify the maximum allowed number of non commenting lines in a record. */
161     private int recordMaximum = RECORD_MAX_NCSS;
162 
163     /** Specify the maximum allowed number of non commenting lines in a method. */
164     private int methodMaximum = METHOD_MAX_NCSS;
165 
166     /** List containing the stacked counters. */
167     private Deque<Counter> counters;
168 
169     @Override
170     public int[] getDefaultTokens() {
171         return getRequiredTokens();
172     }
173 
174     @Override
175     public int[] getRequiredTokens() {
176         return new int[] {
177             TokenTypes.CLASS_DEF,
178             TokenTypes.INTERFACE_DEF,
179             TokenTypes.METHOD_DEF,
180             TokenTypes.CTOR_DEF,
181             TokenTypes.INSTANCE_INIT,
182             TokenTypes.STATIC_INIT,
183             TokenTypes.PACKAGE_DEF,
184             TokenTypes.IMPORT,
185             TokenTypes.VARIABLE_DEF,
186             TokenTypes.CTOR_CALL,
187             TokenTypes.SUPER_CTOR_CALL,
188             TokenTypes.LITERAL_IF,
189             TokenTypes.LITERAL_ELSE,
190             TokenTypes.LITERAL_WHILE,
191             TokenTypes.LITERAL_DO,
192             TokenTypes.LITERAL_FOR,
193             TokenTypes.LITERAL_SWITCH,
194             TokenTypes.LITERAL_BREAK,
195             TokenTypes.LITERAL_CONTINUE,
196             TokenTypes.LITERAL_RETURN,
197             TokenTypes.LITERAL_THROW,
198             TokenTypes.LITERAL_SYNCHRONIZED,
199             TokenTypes.LITERAL_CATCH,
200             TokenTypes.LITERAL_FINALLY,
201             TokenTypes.EXPR,
202             TokenTypes.LABELED_STAT,
203             TokenTypes.LITERAL_CASE,
204             TokenTypes.LITERAL_DEFAULT,
205             TokenTypes.RECORD_DEF,
206             TokenTypes.COMPACT_CTOR_DEF,
207         };
208     }
209 
210     @Override
211     public int[] getAcceptableTokens() {
212         return getRequiredTokens();
213     }
214 
215     @Override
216     public void beginTree(DetailAST rootAST) {
217         counters = new ArrayDeque<>();
218 
219         // add a counter for the file
220         counters.push(new Counter());
221     }
222 
223     @Override
224     public void visitToken(DetailAST ast) {
225         final int tokenType = ast.getType();
226 
227         if (tokenType == TokenTypes.CLASS_DEF
228             || tokenType == TokenTypes.RECORD_DEF
229             || isMethodOrCtorOrInitDefinition(tokenType)) {
230             // add a counter for this class/method
231             counters.push(new Counter());
232         }
233 
234         // check if token is countable
235         if (isCountable(ast)) {
236             // increment the stacked counters
237             counters.forEach(Counter::increment);
238         }
239     }
240 
241     @Override
242     public void leaveToken(DetailAST ast) {
243         final int tokenType = ast.getType();
244 
245         if (isMethodOrCtorOrInitDefinition(tokenType)) {
246             // pop counter from the stack
247             final Counter counter = counters.pop();
248 
249             final int count = counter.getCount();
250             if (count > methodMaximum) {
251                 log(ast, MSG_METHOD, count, methodMaximum);
252             }
253         }
254         else if (tokenType == TokenTypes.CLASS_DEF) {
255             // pop counter from the stack
256             final Counter counter = counters.pop();
257 
258             final int count = counter.getCount();
259             if (count > classMaximum) {
260                 log(ast, MSG_CLASS, count, classMaximum);
261             }
262         }
263         else if (tokenType == TokenTypes.RECORD_DEF) {
264             // pop counter from the stack
265             final Counter counter = counters.pop();
266 
267             final int count = counter.getCount();
268             if (count > recordMaximum) {
269                 log(ast, MSG_RECORD, count, recordMaximum);
270             }
271         }
272     }
273 
274     @Override
275     public void finishTree(DetailAST rootAST) {
276         // pop counter from the stack
277         final Counter counter = counters.pop();
278 
279         final int count = counter.getCount();
280         if (count > fileMaximum) {
281             log(rootAST, MSG_FILE, count, fileMaximum);
282         }
283     }
284 
285     /**
286      * Setter to specify the maximum allowed number of non commenting lines
287      * in a file including all top level and nested classes.
288      *
289      * @param fileMaximum
290      *            the maximum ncss
291      * @since 3.5
292      */
293     public void setFileMaximum(int fileMaximum) {
294         this.fileMaximum = fileMaximum;
295     }
296 
297     /**
298      * Setter to specify the maximum allowed number of non commenting lines in a class.
299      *
300      * @param classMaximum
301      *            the maximum ncss
302      * @since 3.5
303      */
304     public void setClassMaximum(int classMaximum) {
305         this.classMaximum = classMaximum;
306     }
307 
308     /**
309      * Setter to specify the maximum allowed number of non commenting lines in a record.
310      *
311      * @param recordMaximum
312      *            the maximum ncss
313      * @since 8.36
314      */
315     public void setRecordMaximum(int recordMaximum) {
316         this.recordMaximum = recordMaximum;
317     }
318 
319     /**
320      * Setter to specify the maximum allowed number of non commenting lines in a method.
321      *
322      * @param methodMaximum
323      *            the maximum ncss
324      * @since 3.5
325      */
326     public void setMethodMaximum(int methodMaximum) {
327         this.methodMaximum = methodMaximum;
328     }
329 
330     /**
331      * Checks if a token is countable for the ncss metric.
332      *
333      * @param ast
334      *            the AST
335      * @return true if the token is countable
336      */
337     private static boolean isCountable(DetailAST ast) {
338         boolean countable = true;
339 
340         final int tokenType = ast.getType();
341 
342         // check if an expression is countable
343         if (tokenType == TokenTypes.EXPR) {
344             countable = isExpressionCountable(ast);
345         }
346         // check if a variable definition is countable
347         else if (tokenType == TokenTypes.VARIABLE_DEF) {
348             countable = isVariableDefCountable(ast);
349         }
350         return countable;
351     }
352 
353     /**
354      * Checks if a variable definition is countable.
355      *
356      * @param ast the AST
357      * @return true if the variable definition is countable, false otherwise
358      */
359     private static boolean isVariableDefCountable(DetailAST ast) {
360         boolean countable = false;
361 
362         // count variable definitions only if they are direct child to a slist or
363         // object block
364         final int parentType = ast.getParent().getType();
365 
366         if (parentType == TokenTypes.SLIST
367             || parentType == TokenTypes.OBJBLOCK) {
368             final DetailAST prevSibling = ast.getPreviousSibling();
369 
370             // is countable if no previous sibling is found or
371             // the sibling is no COMMA.
372             // This is done because multiple assignment on one line are counted
373             // as 1
374             countable = prevSibling == null
375                     || prevSibling.getType() != TokenTypes.COMMA;
376         }
377 
378         return countable;
379     }
380 
381     /**
382      * Checks if an expression is countable for the ncss metric.
383      *
384      * @param ast the AST
385      * @return true if the expression is countable, false otherwise
386      */
387     private static boolean isExpressionCountable(DetailAST ast) {
388         final boolean countable;
389 
390         // count expressions only if they are direct child to a slist (method
391         // body, for loop...)
392         // or direct child of label,if,else,do,while,for
393         final int parentType = ast.getParent().getType();
394         switch (parentType) {
395             case TokenTypes.SLIST:
396             case TokenTypes.LABELED_STAT:
397             case TokenTypes.LITERAL_FOR:
398             case TokenTypes.LITERAL_DO:
399             case TokenTypes.LITERAL_WHILE:
400             case TokenTypes.LITERAL_IF:
401             case TokenTypes.LITERAL_ELSE:
402                 // don't count if or loop conditions
403                 final DetailAST prevSibling = ast.getPreviousSibling();
404                 countable = prevSibling == null
405                     || prevSibling.getType() != TokenTypes.LPAREN;
406                 break;
407             default:
408                 countable = false;
409                 break;
410         }
411         return countable;
412     }
413 
414     /**
415      * Checks if a token is a method, constructor, or compact constructor definition.
416      *
417      * @param tokenType the type of token we are checking
418      * @return true if token type is method or ctor definition, false otherwise
419      */
420     private static boolean isMethodOrCtorOrInitDefinition(int tokenType) {
421         return tokenType == TokenTypes.METHOD_DEF
422                 || tokenType == TokenTypes.COMPACT_CTOR_DEF
423                 || tokenType == TokenTypes.CTOR_DEF
424                 || tokenType == TokenTypes.STATIC_INIT
425                 || tokenType == TokenTypes.INSTANCE_INIT;
426     }
427 
428     /**
429      * Class representing a counter.
430      *
431      */
432     private static final class Counter {
433 
434         /** The counters internal integer. */
435         private int count;
436 
437         /**
438          * Increments the counter.
439          */
440         public void increment() {
441             count++;
442         }
443 
444         /**
445          * Gets the counters value.
446          *
447          * @return the counter
448          */
449         public int getCount() {
450             return count;
451         }
452 
453     }
454 
455 }