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 }