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