001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks.metrics; 021 022import java.util.ArrayDeque; 023import java.util.Deque; 024 025import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 026import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 027import com.puppycrawl.tools.checkstyle.api.DetailAST; 028import com.puppycrawl.tools.checkstyle.api.TokenTypes; 029 030/** 031 * <div> 032 * Determines complexity of methods, classes and files by counting 033 * the Non Commenting Source Statements (NCSS). This check adheres to the 034 * <a href="http://www.kclee.de/clemens/java/javancss/#specification">specification</a> 035 * for the <a href="http://www.kclee.de/clemens/java/javancss/">JavaNCSS-Tool</a> 036 * written by <b>Chr. Clemens Lee</b>. 037 * </div> 038 * 039 * <p> 040 * Roughly said the NCSS metric is calculated by counting the source lines which are 041 * not comments, (nearly) equivalent to counting the semicolons and opening curly braces. 042 * </p> 043 * 044 * <p> 045 * The NCSS for a class is summarized from the NCSS of all its methods, the NCSS 046 * of its nested classes and the number of member variable declarations. 047 * </p> 048 * 049 * <p> 050 * The NCSS for a file is summarized from the ncss of all its top level classes, 051 * the number of imports and the package declaration. 052 * </p> 053 * 054 * <p> 055 * Rationale: Too large methods and classes are hard to read and costly to maintain. 056 * A large NCSS number often means that a method or class has too many responsibilities 057 * and/or functionalities which should be decomposed into smaller units. 058 * </p> 059 * 060 * <p> 061 * Here is a breakdown of what exactly is counted and not counted: 062 * </p> 063 * <div class="wrapper"> 064 * <table> 065 * <caption>JavaNCSS metrics</caption> 066 * <thead><tr><th>Structure</th><th>NCSS Count</th><th>Notes</th></tr></thead> 067 * <tbody> 068 * <tr><td>Package declaration</td><td>1</td> 069 * <td>Counted at the terminating semicolon.</td></tr> 070 * <tr><td>Import declaration</td><td>1</td> 071 * <td>Each single, static, or wildcard import counts as 1.</td></tr> 072 * <tr><td>Class, Interface, Annotation ({@code @interface})</td><td>1</td> 073 * <td>Counted at the opening curly brace of the body.</td></tr> 074 * <tr><td>Method, Constructor</td><td>1</td> 075 * <td>Counted at the declaration.</td></tr> 076 * <tr><td>Static initializer, Instance initializer</td><td>1</td> 077 * <td>Both {@code static {}} and bare {@code {}} initializer blocks count as 1.</td></tr> 078 * <tr><td>Annotation type member</td><td>1</td> 079 * <td>Each method-like member declaration inside {@code @interface} counts as 1. 080 * A standalone {@code ;} inside {@code @interface} also counts as 1.</td></tr> 081 * <tr><td>Variable declaration</td><td>1</td> 082 * <td>1 per statement regardless of how many variables are declared on that line. 083 * {@code int x, y;} counts as 1.</td></tr> 084 * <tr><td>{@code if}</td><td>1</td> 085 * <td>The {@code if} keyword counts as 1.</td></tr> 086 * <tr><td>{@code else}, {@code else if}</td><td>1</td> 087 * <td>The {@code else} keyword counts as 1, separate from the {@code if} count.</td></tr> 088 * <tr><td>{@code while}, {@code do}, {@code for}</td><td>1</td> 089 * <td>The keyword header counts as 1.</td></tr> 090 * <tr><td>{@code switch}</td><td>1</td> 091 * <td>The {@code switch} keyword counts as 1.</td></tr> 092 * <tr><td>{@code case}, {@code default}</td><td>1</td> 093 * <td>Every case and default label adds 1.</td></tr> 094 * <tr><td>{@code try}</td><td>0</td> 095 * <td>The {@code try} keyword itself does not count.</td></tr> 096 * <tr><td>{@code catch}</td><td>1</td> 097 * <td>Each catch block counts as 1.</td></tr> 098 * <tr><td>{@code finally}</td><td>1</td> 099 * <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 128public 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}