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" file. 132 */ 133 public static final String MSG_METHOD = "ncss.method"; 134 135 /** 136 * A key is pointing to the warning message text in "messages.properties" file. 137 */ 138 public static final String MSG_CLASS = "ncss.class"; 139 140 /** 141 * A key is pointing to the warning message text in "messages.properties" file. 142 */ 143 public static final String MSG_RECORD = "ncss.record"; 144 145 /** 146 * A key is pointing to the warning message text in "messages.properties" file. 147 */ 148 public static final String MSG_FILE = "ncss.file"; 149 150 /** Default constant for max file ncss. */ 151 private static final int FILE_MAX_NCSS = 2000; 152 153 /** Default constant for max file ncss. */ 154 private static final int CLASS_MAX_NCSS = 1500; 155 156 /** Default constant for max record ncss. */ 157 private static final int RECORD_MAX_NCSS = 150; 158 159 /** Default constant for max method ncss. */ 160 private static final int METHOD_MAX_NCSS = 50; 161 162 /** 163 * Specify the maximum allowed number of non commenting lines in a file 164 * including all top level and nested classes. 165 */ 166 private int fileMaximum = FILE_MAX_NCSS; 167 168 /** Specify the maximum allowed number of non commenting lines in a class. */ 169 private int classMaximum = CLASS_MAX_NCSS; 170 171 /** Specify the maximum allowed number of non commenting lines in a record. */ 172 private int recordMaximum = RECORD_MAX_NCSS; 173 174 /** Specify the maximum allowed number of non commenting lines in a method. */ 175 private int methodMaximum = METHOD_MAX_NCSS; 176 177 /** List containing the stacked counters. */ 178 private Deque<Counter> counters; 179 180 /** 181 * Creates a new {@code JavaNCSSCheck} instance. 182 */ 183 public JavaNCSSCheck() { 184 // no code by default 185 } 186 187 @Override 188 public int[] getDefaultTokens() { 189 return getRequiredTokens(); 190 } 191 192 @Override 193 public int[] getRequiredTokens() { 194 return new int[] { 195 TokenTypes.CLASS_DEF, 196 TokenTypes.INTERFACE_DEF, 197 TokenTypes.METHOD_DEF, 198 TokenTypes.CTOR_DEF, 199 TokenTypes.INSTANCE_INIT, 200 TokenTypes.STATIC_INIT, 201 TokenTypes.PACKAGE_DEF, 202 TokenTypes.IMPORT, 203 TokenTypes.VARIABLE_DEF, 204 TokenTypes.CTOR_CALL, 205 TokenTypes.SUPER_CTOR_CALL, 206 TokenTypes.LITERAL_IF, 207 TokenTypes.LITERAL_ELSE, 208 TokenTypes.LITERAL_WHILE, 209 TokenTypes.LITERAL_DO, 210 TokenTypes.LITERAL_FOR, 211 TokenTypes.LITERAL_SWITCH, 212 TokenTypes.LITERAL_BREAK, 213 TokenTypes.LITERAL_CONTINUE, 214 TokenTypes.LITERAL_RETURN, 215 TokenTypes.LITERAL_THROW, 216 TokenTypes.LITERAL_SYNCHRONIZED, 217 TokenTypes.LITERAL_CATCH, 218 TokenTypes.LITERAL_FINALLY, 219 TokenTypes.EXPR, 220 TokenTypes.LABELED_STAT, 221 TokenTypes.LITERAL_CASE, 222 TokenTypes.LITERAL_DEFAULT, 223 TokenTypes.RECORD_DEF, 224 TokenTypes.COMPACT_CTOR_DEF, 225 }; 226 } 227 228 @Override 229 public int[] getAcceptableTokens() { 230 return getRequiredTokens(); 231 } 232 233 @Override 234 public void beginTree(DetailAST rootAST) { 235 counters = new ArrayDeque<>(); 236 237 // add a counter for the file 238 counters.push(new Counter()); 239 } 240 241 @Override 242 public void visitToken(DetailAST ast) { 243 final int tokenType = ast.getType(); 244 245 if (tokenType == TokenTypes.CLASS_DEF 246 || tokenType == TokenTypes.RECORD_DEF 247 || isMethodOrCtorOrInitDefinition(tokenType)) { 248 // add a counter for this class/method 249 counters.push(new Counter()); 250 } 251 252 // check if token is countable 253 if (isCountable(ast)) { 254 // increment the stacked counters 255 counters.forEach(Counter::increment); 256 } 257 } 258 259 @Override 260 public void leaveToken(DetailAST ast) { 261 final int tokenType = ast.getType(); 262 263 if (isMethodOrCtorOrInitDefinition(tokenType)) { 264 // pop counter from the stack 265 final Counter counter = counters.pop(); 266 267 final int count = counter.getCount(); 268 if (count > methodMaximum) { 269 log(ast, MSG_METHOD, count, methodMaximum); 270 } 271 } 272 else if (tokenType == TokenTypes.CLASS_DEF) { 273 // pop counter from the stack 274 final Counter counter = counters.pop(); 275 276 final int count = counter.getCount(); 277 if (count > classMaximum) { 278 log(ast, MSG_CLASS, count, classMaximum); 279 } 280 } 281 else if (tokenType == TokenTypes.RECORD_DEF) { 282 // pop counter from the stack 283 final Counter counter = counters.pop(); 284 285 final int count = counter.getCount(); 286 if (count > recordMaximum) { 287 log(ast, MSG_RECORD, count, recordMaximum); 288 } 289 } 290 } 291 292 @Override 293 public void finishTree(DetailAST rootAST) { 294 // pop counter from the stack 295 final Counter counter = counters.pop(); 296 297 final int count = counter.getCount(); 298 if (count > fileMaximum) { 299 log(rootAST, MSG_FILE, count, fileMaximum); 300 } 301 } 302 303 /** 304 * Setter to specify the maximum allowed number of non commenting lines 305 * in a file including all top level and nested classes. 306 * 307 * @param fileMaximum 308 * the maximum ncss 309 * @since 3.5 310 */ 311 public void setFileMaximum(int fileMaximum) { 312 this.fileMaximum = fileMaximum; 313 } 314 315 /** 316 * Setter to specify the maximum allowed number of non commenting lines in a class. 317 * 318 * @param classMaximum 319 * the maximum ncss 320 * @since 3.5 321 */ 322 public void setClassMaximum(int classMaximum) { 323 this.classMaximum = classMaximum; 324 } 325 326 /** 327 * Setter to specify the maximum allowed number of non commenting lines in a record. 328 * 329 * @param recordMaximum 330 * the maximum ncss 331 * @since 8.36 332 */ 333 public void setRecordMaximum(int recordMaximum) { 334 this.recordMaximum = recordMaximum; 335 } 336 337 /** 338 * Setter to specify the maximum allowed number of non commenting lines in a method. 339 * 340 * @param methodMaximum 341 * the maximum ncss 342 * @since 3.5 343 */ 344 public void setMethodMaximum(int methodMaximum) { 345 this.methodMaximum = methodMaximum; 346 } 347 348 /** 349 * Checks if a token is countable for the ncss metric. 350 * 351 * @param ast 352 * the AST 353 * @return true if the token is countable 354 */ 355 private static boolean isCountable(DetailAST ast) { 356 boolean countable = true; 357 358 final int tokenType = ast.getType(); 359 360 // check if an expression is countable 361 if (tokenType == TokenTypes.EXPR) { 362 countable = isExpressionCountable(ast); 363 } 364 // check if a variable definition is countable 365 else if (tokenType == TokenTypes.VARIABLE_DEF) { 366 countable = isVariableDefCountable(ast); 367 } 368 return countable; 369 } 370 371 /** 372 * Checks if a variable definition is countable. 373 * 374 * @param ast the AST 375 * @return true if the variable definition is countable, false otherwise 376 */ 377 private static boolean isVariableDefCountable(DetailAST ast) { 378 boolean countable = false; 379 380 // count variable definitions only if they are direct child to a slist or 381 // object block 382 final int parentType = ast.getParent().getType(); 383 384 if (parentType == TokenTypes.SLIST 385 || parentType == TokenTypes.OBJBLOCK) { 386 final DetailAST prevSibling = ast.getPreviousSibling(); 387 388 // is countable if no previous sibling is found or 389 // the sibling is no COMMA. 390 // This is done because multiple assignment on one line are counted 391 // as 1 392 countable = prevSibling == null 393 || prevSibling.getType() != TokenTypes.COMMA; 394 } 395 396 return countable; 397 } 398 399 /** 400 * Checks if an expression is countable for the ncss metric. 401 * 402 * @param ast the AST 403 * @return true if the expression is countable, false otherwise 404 */ 405 private static boolean isExpressionCountable(DetailAST ast) { 406 407 // count expressions only if they are direct child to a slist (method 408 // body, for loop...) 409 // or direct child of label,if,else,do,while,for 410 final int parentType = ast.getParent().getType(); 411 return switch (parentType) { 412 case TokenTypes.SLIST, TokenTypes.LABELED_STAT, TokenTypes.LITERAL_FOR, 413 TokenTypes.LITERAL_DO, 414 TokenTypes.LITERAL_WHILE, TokenTypes.LITERAL_IF, TokenTypes.LITERAL_ELSE -> { 415 // don't count if or loop conditions 416 final DetailAST prevSibling = ast.getPreviousSibling(); 417 yield prevSibling == null 418 || prevSibling.getType() != TokenTypes.LPAREN; 419 } 420 default -> false; 421 }; 422 } 423 424 /** 425 * Checks if a token is a method, constructor, or compact constructor definition. 426 * 427 * @param tokenType the type of token we are checking 428 * @return true if token type is method or ctor definition, false otherwise 429 */ 430 private static boolean isMethodOrCtorOrInitDefinition(int tokenType) { 431 return tokenType == TokenTypes.METHOD_DEF 432 || tokenType == TokenTypes.COMPACT_CTOR_DEF 433 || tokenType == TokenTypes.CTOR_DEF 434 || tokenType == TokenTypes.STATIC_INIT 435 || tokenType == TokenTypes.INSTANCE_INIT; 436 } 437 438 /** 439 * Class representing a counter. 440 * 441 */ 442 private static final class Counter { 443 444 /** The counters internal integer. */ 445 private int count; 446 447 /** 448 * Creates a new {@code Counter} instance. 449 */ 450 private Counter() { 451 // no code by default 452 } 453 454 /** 455 * Increments the counter. 456 */ 457 /* package */ void increment() { 458 count++; 459 } 460 461 /** 462 * Gets the counters value. 463 * 464 * @return the counter 465 */ 466 /* package */ int getCount() { 467 return count; 468 } 469 470 } 471 472}