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 @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}