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 }