View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 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   *
60   * <p>
61   * Here is a breakdown of what exactly is counted and not counted:
62   * </p>
63   * <div class="wrapper">
64   * <table>
65   * <caption>JavaNCSS metrics</caption>
66   * <thead><tr><th>Structure</th><th>NCSS Count</th><th>Notes</th></tr></thead>
67   * <tbody>
68   * <tr><td>Package declaration</td><td>1</td>
69   * <td>Counted at the terminating semicolon.</td></tr>
70   * <tr><td>Import declaration</td><td>1</td>
71   * <td>Each single, static, or wildcard import counts as 1.</td></tr>
72   * <tr><td>Class, Interface, Annotation ({@code @interface})</td><td>1</td>
73   * <td>Counted at the opening curly brace of the body.</td></tr>
74   * <tr><td>Method, Constructor</td><td>1</td>
75   * <td>Counted at the declaration.</td></tr>
76   * <tr><td>Static initializer, Instance initializer</td><td>1</td>
77   * <td>Both {@code static {}} and bare {@code {}} initializer blocks count as 1.</td></tr>
78   * <tr><td>Annotation type member</td><td>1</td>
79   * <td>Each method-like member declaration inside {@code @interface} counts as 1.
80   * A standalone {@code ;} inside {@code @interface} also counts as 1.</td></tr>
81   * <tr><td>Variable declaration</td><td>1</td>
82   * <td>1 per statement regardless of how many variables are declared on that line.
83   * {@code int x, y;} counts as 1.</td></tr>
84   * <tr><td>{@code if}</td><td>1</td>
85   * <td>The {@code if} keyword counts as 1.</td></tr>
86   * <tr><td>{@code else}, {@code else if}</td><td>1</td>
87   * <td>The {@code else} keyword counts as 1, separate from the {@code if} count.</td></tr>
88   * <tr><td>{@code while}, {@code do}, {@code for}</td><td>1</td>
89   * <td>The keyword header counts as 1.</td></tr>
90   * <tr><td>{@code switch}</td><td>1</td>
91   * <td>The {@code switch} keyword counts as 1.</td></tr>
92   * <tr><td>{@code case}, {@code default}</td><td>1</td>
93   * <td>Every case and default label adds 1.</td></tr>
94   * <tr><td>{@code try}</td><td>0</td>
95   * <td>The {@code try} keyword itself does not count.</td></tr>
96   * <tr><td>{@code catch}</td><td>1</td>
97   * <td>Each catch block counts as 1.</td></tr>
98   * <tr><td>{@code finally}</td><td>1</td>
99   * <td>The finally block counts as 1.</td></tr>
100  * <tr><td>{@code synchronized}</td><td>1</td>
101  * <td>The synchronized statement counts as 1.</td></tr>
102  * <tr><td>{@code return}, {@code break}, {@code continue}, {@code throw}</td><td>1</td>
103  * <td>Each counts as 1.</td></tr>
104  * <tr><td>{@code assert}</td><td>1</td>
105  * <td>Each assert statement counts as 1, with or without a message expression.</td></tr>
106  * <tr><td>Labeled statement</td><td>1</td>
107  * <td>{@code label: statement} counts as 1.</td></tr>
108  * <tr><td>Explicit constructor invocation</td><td>1</td>
109  * <td>{@code this()} or {@code super()} calls inside a constructor body
110  * each count as 1.</td></tr>
111  * <tr><td>Expression statements (assignments, method calls)</td><td>1</td>
112  * <td>Statement-level expressions terminated by {@code ;} count as 1.
113  * A method call inside a {@code return} does not add an extra count.</td></tr>
114  * <tr><td>Empty blocks {}</td><td>0</td>
115  * <td>Empty curly braces do not increase the count.</td></tr>
116  * <tr><td>Empty statements ;</td><td>0</td>
117  * <td>Standalone semicolons outside of {@code @interface} do not increase
118  * the count.</td></tr>
119  * </tbody>
120  * </table>
121  * </div>
122  *
123  * @since 3.5
124  */
125 // -@cs[AbbreviationAsWordInName] We can not change it as,
126 // check's name is a part of API (used in configurations).
127 @FileStatefulCheck
128 public class JavaNCSSCheck extends AbstractCheck {
129 
130     /**
131      * A key is pointing to the warning message text in "messages.properties"
132      * file.
133      */
134     public static final String MSG_METHOD = "ncss.method";
135 
136     /**
137      * A key is pointing to the warning message text in "messages.properties"
138      * file.
139      */
140     public static final String MSG_CLASS = "ncss.class";
141 
142     /**
143      * A key is pointing to the warning message text in "messages.properties"
144      * file.
145      */
146     public static final String MSG_RECORD = "ncss.record";
147 
148     /**
149      * A key is pointing to the warning message text in "messages.properties"
150      * file.
151      */
152     public static final String MSG_FILE = "ncss.file";
153 
154     /** Default constant for max file ncss. */
155     private static final int FILE_MAX_NCSS = 2000;
156 
157     /** Default constant for max file ncss. */
158     private static final int CLASS_MAX_NCSS = 1500;
159 
160     /** Default constant for max record ncss. */
161     private static final int RECORD_MAX_NCSS = 150;
162 
163     /** Default constant for max method ncss. */
164     private static final int METHOD_MAX_NCSS = 50;
165 
166     /**
167      * Specify the maximum allowed number of non commenting lines in a file
168      * including all top level and nested classes.
169      */
170     private int fileMaximum = FILE_MAX_NCSS;
171 
172     /** Specify the maximum allowed number of non commenting lines in a class. */
173     private int classMaximum = CLASS_MAX_NCSS;
174 
175     /** Specify the maximum allowed number of non commenting lines in a record. */
176     private int recordMaximum = RECORD_MAX_NCSS;
177 
178     /** Specify the maximum allowed number of non commenting lines in a method. */
179     private int methodMaximum = METHOD_MAX_NCSS;
180 
181     /** List containing the stacked counters. */
182     private Deque<Counter> counters;
183 
184     @Override
185     public int[] getDefaultTokens() {
186         return getRequiredTokens();
187     }
188 
189     @Override
190     public int[] getRequiredTokens() {
191         return new int[] {
192             TokenTypes.CLASS_DEF,
193             TokenTypes.INTERFACE_DEF,
194             TokenTypes.METHOD_DEF,
195             TokenTypes.CTOR_DEF,
196             TokenTypes.INSTANCE_INIT,
197             TokenTypes.STATIC_INIT,
198             TokenTypes.PACKAGE_DEF,
199             TokenTypes.IMPORT,
200             TokenTypes.VARIABLE_DEF,
201             TokenTypes.CTOR_CALL,
202             TokenTypes.SUPER_CTOR_CALL,
203             TokenTypes.LITERAL_IF,
204             TokenTypes.LITERAL_ELSE,
205             TokenTypes.LITERAL_WHILE,
206             TokenTypes.LITERAL_DO,
207             TokenTypes.LITERAL_FOR,
208             TokenTypes.LITERAL_SWITCH,
209             TokenTypes.LITERAL_BREAK,
210             TokenTypes.LITERAL_CONTINUE,
211             TokenTypes.LITERAL_RETURN,
212             TokenTypes.LITERAL_THROW,
213             TokenTypes.LITERAL_SYNCHRONIZED,
214             TokenTypes.LITERAL_CATCH,
215             TokenTypes.LITERAL_FINALLY,
216             TokenTypes.EXPR,
217             TokenTypes.LABELED_STAT,
218             TokenTypes.LITERAL_CASE,
219             TokenTypes.LITERAL_DEFAULT,
220             TokenTypes.RECORD_DEF,
221             TokenTypes.COMPACT_CTOR_DEF,
222         };
223     }
224 
225     @Override
226     public int[] getAcceptableTokens() {
227         return getRequiredTokens();
228     }
229 
230     @Override
231     public void beginTree(DetailAST rootAST) {
232         counters = new ArrayDeque<>();
233 
234         // add a counter for the file
235         counters.push(new Counter());
236     }
237 
238     @Override
239     public void visitToken(DetailAST ast) {
240         final int tokenType = ast.getType();
241 
242         if (tokenType == TokenTypes.CLASS_DEF
243             || tokenType == TokenTypes.RECORD_DEF
244             || isMethodOrCtorOrInitDefinition(tokenType)) {
245             // add a counter for this class/method
246             counters.push(new Counter());
247         }
248 
249         // check if token is countable
250         if (isCountable(ast)) {
251             // increment the stacked counters
252             counters.forEach(Counter::increment);
253         }
254     }
255 
256     @Override
257     public void leaveToken(DetailAST ast) {
258         final int tokenType = ast.getType();
259 
260         if (isMethodOrCtorOrInitDefinition(tokenType)) {
261             // pop counter from the stack
262             final Counter counter = counters.pop();
263 
264             final int count = counter.getCount();
265             if (count > methodMaximum) {
266                 log(ast, MSG_METHOD, count, methodMaximum);
267             }
268         }
269         else if (tokenType == TokenTypes.CLASS_DEF) {
270             // pop counter from the stack
271             final Counter counter = counters.pop();
272 
273             final int count = counter.getCount();
274             if (count > classMaximum) {
275                 log(ast, MSG_CLASS, count, classMaximum);
276             }
277         }
278         else if (tokenType == TokenTypes.RECORD_DEF) {
279             // pop counter from the stack
280             final Counter counter = counters.pop();
281 
282             final int count = counter.getCount();
283             if (count > recordMaximum) {
284                 log(ast, MSG_RECORD, count, recordMaximum);
285             }
286         }
287     }
288 
289     @Override
290     public void finishTree(DetailAST rootAST) {
291         // pop counter from the stack
292         final Counter counter = counters.pop();
293 
294         final int count = counter.getCount();
295         if (count > fileMaximum) {
296             log(rootAST, MSG_FILE, count, fileMaximum);
297         }
298     }
299 
300     /**
301      * Setter to specify the maximum allowed number of non commenting lines
302      * in a file including all top level and nested classes.
303      *
304      * @param fileMaximum
305      *            the maximum ncss
306      * @since 3.5
307      */
308     public void setFileMaximum(int fileMaximum) {
309         this.fileMaximum = fileMaximum;
310     }
311 
312     /**
313      * Setter to specify the maximum allowed number of non commenting lines in a class.
314      *
315      * @param classMaximum
316      *            the maximum ncss
317      * @since 3.5
318      */
319     public void setClassMaximum(int classMaximum) {
320         this.classMaximum = classMaximum;
321     }
322 
323     /**
324      * Setter to specify the maximum allowed number of non commenting lines in a record.
325      *
326      * @param recordMaximum
327      *            the maximum ncss
328      * @since 8.36
329      */
330     public void setRecordMaximum(int recordMaximum) {
331         this.recordMaximum = recordMaximum;
332     }
333 
334     /**
335      * Setter to specify the maximum allowed number of non commenting lines in a method.
336      *
337      * @param methodMaximum
338      *            the maximum ncss
339      * @since 3.5
340      */
341     public void setMethodMaximum(int methodMaximum) {
342         this.methodMaximum = methodMaximum;
343     }
344 
345     /**
346      * Checks if a token is countable for the ncss metric.
347      *
348      * @param ast
349      *            the AST
350      * @return true if the token is countable
351      */
352     private static boolean isCountable(DetailAST ast) {
353         boolean countable = true;
354 
355         final int tokenType = ast.getType();
356 
357         // check if an expression is countable
358         if (tokenType == TokenTypes.EXPR) {
359             countable = isExpressionCountable(ast);
360         }
361         // check if a variable definition is countable
362         else if (tokenType == TokenTypes.VARIABLE_DEF) {
363             countable = isVariableDefCountable(ast);
364         }
365         return countable;
366     }
367 
368     /**
369      * Checks if a variable definition is countable.
370      *
371      * @param ast the AST
372      * @return true if the variable definition is countable, false otherwise
373      */
374     private static boolean isVariableDefCountable(DetailAST ast) {
375         boolean countable = false;
376 
377         // count variable definitions only if they are direct child to a slist or
378         // object block
379         final int parentType = ast.getParent().getType();
380 
381         if (parentType == TokenTypes.SLIST
382             || parentType == TokenTypes.OBJBLOCK) {
383             final DetailAST prevSibling = ast.getPreviousSibling();
384 
385             // is countable if no previous sibling is found or
386             // the sibling is no COMMA.
387             // This is done because multiple assignment on one line are counted
388             // as 1
389             countable = prevSibling == null
390                     || prevSibling.getType() != TokenTypes.COMMA;
391         }
392 
393         return countable;
394     }
395 
396     /**
397      * Checks if an expression is countable for the ncss metric.
398      *
399      * @param ast the AST
400      * @return true if the expression is countable, false otherwise
401      */
402     private static boolean isExpressionCountable(DetailAST ast) {
403 
404         // count expressions only if they are direct child to a slist (method
405         // body, for loop...)
406         // or direct child of label,if,else,do,while,for
407         final int parentType = ast.getParent().getType();
408         return switch (parentType) {
409             case TokenTypes.SLIST, TokenTypes.LABELED_STAT, TokenTypes.LITERAL_FOR,
410                  TokenTypes.LITERAL_DO,
411                  TokenTypes.LITERAL_WHILE, TokenTypes.LITERAL_IF, TokenTypes.LITERAL_ELSE -> {
412                 // don't count if or loop conditions
413                 final DetailAST prevSibling = ast.getPreviousSibling();
414                 yield prevSibling == null
415                         || prevSibling.getType() != TokenTypes.LPAREN;
416             }
417             default -> false;
418         };
419     }
420 
421     /**
422      * Checks if a token is a method, constructor, or compact constructor definition.
423      *
424      * @param tokenType the type of token we are checking
425      * @return true if token type is method or ctor definition, false otherwise
426      */
427     private static boolean isMethodOrCtorOrInitDefinition(int tokenType) {
428         return tokenType == TokenTypes.METHOD_DEF
429                 || tokenType == TokenTypes.COMPACT_CTOR_DEF
430                 || tokenType == TokenTypes.CTOR_DEF
431                 || tokenType == TokenTypes.STATIC_INIT
432                 || tokenType == TokenTypes.INSTANCE_INIT;
433     }
434 
435     /**
436      * Class representing a counter.
437      *
438      */
439     private static final class Counter {
440 
441         /** The counters internal integer. */
442         private int count;
443 
444         /**
445          * Increments the counter.
446          */
447         /* package */ void increment() {
448             count++;
449         }
450 
451         /**
452          * Gets the counters value.
453          *
454          * @return the counter
455          */
456         /* package */ int getCount() {
457             return count;
458         }
459 
460     }
461 
462 }