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 }