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