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     /**
185      * Creates a new {@code JavaNCSSCheck} instance.
186      */
187     public JavaNCSSCheck() {
188         // no code by default
189     }
190 
191     @Override
192     public int[] getDefaultTokens() {
193         return getRequiredTokens();
194     }
195 
196     @Override
197     public int[] getRequiredTokens() {
198         return new int[] {
199             TokenTypes.CLASS_DEF,
200             TokenTypes.INTERFACE_DEF,
201             TokenTypes.METHOD_DEF,
202             TokenTypes.CTOR_DEF,
203             TokenTypes.INSTANCE_INIT,
204             TokenTypes.STATIC_INIT,
205             TokenTypes.PACKAGE_DEF,
206             TokenTypes.IMPORT,
207             TokenTypes.VARIABLE_DEF,
208             TokenTypes.CTOR_CALL,
209             TokenTypes.SUPER_CTOR_CALL,
210             TokenTypes.LITERAL_IF,
211             TokenTypes.LITERAL_ELSE,
212             TokenTypes.LITERAL_WHILE,
213             TokenTypes.LITERAL_DO,
214             TokenTypes.LITERAL_FOR,
215             TokenTypes.LITERAL_SWITCH,
216             TokenTypes.LITERAL_BREAK,
217             TokenTypes.LITERAL_CONTINUE,
218             TokenTypes.LITERAL_RETURN,
219             TokenTypes.LITERAL_THROW,
220             TokenTypes.LITERAL_SYNCHRONIZED,
221             TokenTypes.LITERAL_CATCH,
222             TokenTypes.LITERAL_FINALLY,
223             TokenTypes.EXPR,
224             TokenTypes.LABELED_STAT,
225             TokenTypes.LITERAL_CASE,
226             TokenTypes.LITERAL_DEFAULT,
227             TokenTypes.RECORD_DEF,
228             TokenTypes.COMPACT_CTOR_DEF,
229         };
230     }
231 
232     @Override
233     public int[] getAcceptableTokens() {
234         return getRequiredTokens();
235     }
236 
237     @Override
238     public void beginTree(DetailAST rootAST) {
239         counters = new ArrayDeque<>();
240 
241         // add a counter for the file
242         counters.push(new Counter());
243     }
244 
245     @Override
246     public void visitToken(DetailAST ast) {
247         final int tokenType = ast.getType();
248 
249         if (tokenType == TokenTypes.CLASS_DEF
250             || tokenType == TokenTypes.RECORD_DEF
251             || isMethodOrCtorOrInitDefinition(tokenType)) {
252             // add a counter for this class/method
253             counters.push(new Counter());
254         }
255 
256         // check if token is countable
257         if (isCountable(ast)) {
258             // increment the stacked counters
259             counters.forEach(Counter::increment);
260         }
261     }
262 
263     @Override
264     public void leaveToken(DetailAST ast) {
265         final int tokenType = ast.getType();
266 
267         if (isMethodOrCtorOrInitDefinition(tokenType)) {
268             // pop counter from the stack
269             final Counter counter = counters.pop();
270 
271             final int count = counter.getCount();
272             if (count > methodMaximum) {
273                 log(ast, MSG_METHOD, count, methodMaximum);
274             }
275         }
276         else if (tokenType == TokenTypes.CLASS_DEF) {
277             // pop counter from the stack
278             final Counter counter = counters.pop();
279 
280             final int count = counter.getCount();
281             if (count > classMaximum) {
282                 log(ast, MSG_CLASS, count, classMaximum);
283             }
284         }
285         else if (tokenType == TokenTypes.RECORD_DEF) {
286             // pop counter from the stack
287             final Counter counter = counters.pop();
288 
289             final int count = counter.getCount();
290             if (count > recordMaximum) {
291                 log(ast, MSG_RECORD, count, recordMaximum);
292             }
293         }
294     }
295 
296     @Override
297     public void finishTree(DetailAST rootAST) {
298         // pop counter from the stack
299         final Counter counter = counters.pop();
300 
301         final int count = counter.getCount();
302         if (count > fileMaximum) {
303             log(rootAST, MSG_FILE, count, fileMaximum);
304         }
305     }
306 
307     /**
308      * Setter to specify the maximum allowed number of non commenting lines
309      * in a file including all top level and nested classes.
310      *
311      * @param fileMaximum
312      *            the maximum ncss
313      * @since 3.5
314      */
315     public void setFileMaximum(int fileMaximum) {
316         this.fileMaximum = fileMaximum;
317     }
318 
319     /**
320      * Setter to specify the maximum allowed number of non commenting lines in a class.
321      *
322      * @param classMaximum
323      *            the maximum ncss
324      * @since 3.5
325      */
326     public void setClassMaximum(int classMaximum) {
327         this.classMaximum = classMaximum;
328     }
329 
330     /**
331      * Setter to specify the maximum allowed number of non commenting lines in a record.
332      *
333      * @param recordMaximum
334      *            the maximum ncss
335      * @since 8.36
336      */
337     public void setRecordMaximum(int recordMaximum) {
338         this.recordMaximum = recordMaximum;
339     }
340 
341     /**
342      * Setter to specify the maximum allowed number of non commenting lines in a method.
343      *
344      * @param methodMaximum
345      *            the maximum ncss
346      * @since 3.5
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 a 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 
411         // count expressions only if they are direct child to a slist (method
412         // body, for loop...)
413         // or direct child of label,if,else,do,while,for
414         final int parentType = ast.getParent().getType();
415         return switch (parentType) {
416             case TokenTypes.SLIST, TokenTypes.LABELED_STAT, TokenTypes.LITERAL_FOR,
417                  TokenTypes.LITERAL_DO,
418                  TokenTypes.LITERAL_WHILE, TokenTypes.LITERAL_IF, TokenTypes.LITERAL_ELSE -> {
419                 // don't count if or loop conditions
420                 final DetailAST prevSibling = ast.getPreviousSibling();
421                 yield prevSibling == null
422                         || prevSibling.getType() != TokenTypes.LPAREN;
423             }
424             default -> false;
425         };
426     }
427 
428     /**
429      * Checks if a token is a method, constructor, or compact constructor definition.
430      *
431      * @param tokenType the type of token we are checking
432      * @return true if token type is method or ctor definition, false otherwise
433      */
434     private static boolean isMethodOrCtorOrInitDefinition(int tokenType) {
435         return tokenType == TokenTypes.METHOD_DEF
436                 || tokenType == TokenTypes.COMPACT_CTOR_DEF
437                 || tokenType == TokenTypes.CTOR_DEF
438                 || tokenType == TokenTypes.STATIC_INIT
439                 || tokenType == TokenTypes.INSTANCE_INIT;
440     }
441 
442     /**
443      * Class representing a counter.
444      *
445      */
446     private static final class Counter {
447 
448         /** The counters internal integer. */
449         private int count;
450 
451         /**
452          * Creates a new {@code Counter} instance.
453          */
454         private Counter() {
455             // no code by default
456         }
457 
458         /**
459          * Increments the counter.
460          */
461         /* package */ void increment() {
462             count++;
463         }
464 
465         /**
466          * Gets the counters value.
467          *
468          * @return the counter
469          */
470         /* package */ int getCount() {
471             return count;
472         }
473 
474     }
475 
476 }