001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2023 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 * <p> 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 * </p> 038 * <p> 039 * Roughly said the NCSS metric is calculated by counting the source lines which are 040 * not comments, (nearly) equivalent to counting the semicolons and opening curly braces. 041 * </p> 042 * <p> 043 * The NCSS for a class is summarized from the NCSS of all its methods, the NCSS 044 * of its nested classes and the number of member variable declarations. 045 * </p> 046 * <p> 047 * The NCSS for a file is summarized from the ncss of all its top level classes, 048 * the number of imports and the package declaration. 049 * </p> 050 * <p> 051 * Rationale: Too large methods and classes are hard to read and costly to maintain. 052 * A large NCSS number often means that a method or class has too many responsibilities 053 * and/or functionalities which should be decomposed into smaller units. 054 * </p> 055 * <ul> 056 * <li> 057 * Property {@code methodMaximum} - Specify the maximum allowed number of 058 * non commenting lines in a method. 059 * Type is {@code int}. 060 * Default value is {@code 50}. 061 * </li> 062 * <li> 063 * Property {@code classMaximum} - Specify the maximum allowed number of 064 * non commenting lines in a class. 065 * Type is {@code int}. 066 * Default value is {@code 1500}. 067 * </li> 068 * <li> 069 * Property {@code fileMaximum} - Specify the maximum allowed number of 070 * non commenting lines in a file including all top level and nested classes. 071 * Type is {@code int}. 072 * Default value is {@code 2000}. 073 * </li> 074 * <li> 075 * Property {@code recordMaximum} - Specify the maximum allowed number of 076 * non commenting lines in a record. 077 * Type is {@code int}. 078 * Default value is {@code 150}. 079 * </li> 080 * </ul> 081 * <p> 082 * To configure the check: 083 * </p> 084 * <pre> 085 * <module name="JavaNCSS"/> 086 * </pre> 087 * <p>Example:</p> 088 * <pre> 089 * public void test() { 090 * System.out.println("Line 1"); 091 * // another 48 lines of code 092 * System.out.println("Line 50") // OK 093 * System.out.println("Line 51") // violation, the method crosses 50 non commented lines 094 * } 095 * </pre> 096 * <p> 097 * To configure the check with 40 allowed non commented lines for a method: 098 * </p> 099 * <pre> 100 * <module name="JavaNCSS"> 101 * <property name="methodMaximum" value="40"/> 102 * </module> 103 * </pre> 104 * <p>Example:</p> 105 * <pre> 106 * public void test() { 107 * System.out.println("Line 1"); 108 * // another 38 lines of code 109 * System.out.println("Line 40") // OK 110 * System.out.println("Line 41") // violation, the method crosses 40 non commented lines 111 * } 112 * </pre> 113 * <p> 114 * To configure the check to set limit of non commented lines in class to 100: 115 * </p> 116 * <pre> 117 * <module name="JavaNCSS"> 118 * <property name="classMaximum" value="100"/> 119 * </module> 120 * </pre> 121 * <p>Example:</p> 122 * <pre> 123 * public class Test { 124 * public void test() { 125 * System.out.println("Line 1"); 126 * // another 47 lines of code 127 * System.out.println("Line 49"); 128 * } 129 * 130 * public void test1() { 131 * System.out.println("Line 50"); // OK 132 * // another 47 lines of code 133 * System.out.println("Line 98"); // violation 134 * } 135 * } 136 * </pre> 137 * <p> 138 * To configure the check to set limit of non commented lines in file to 200: 139 * </p> 140 * <pre> 141 * <module name="JavaNCSS"> 142 * <property name="fileMaximum" value="200"/> 143 * </module> 144 * </pre> 145 * <p>Example:</p> 146 * <pre> 147 * public class Test1 { 148 * public void test() { 149 * System.out.println("Line 1"); 150 * // another 48 lines of code 151 * System.out.println("Line 49"); 152 * } 153 * 154 * public void test1() { 155 * System.out.println("Line 50"); 156 * // another 47 lines of code 157 * System.out.println("Line 98"); // OK 158 * } 159 * } 160 * 161 * class Test2 { 162 * public void test() { 163 * System.out.println("Line 150"); // OK 164 * } 165 * 166 * public void test1() { 167 * System.out.println("Line 200"); // violation 168 * } 169 * } 170 * </pre> 171 * <p> 172 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 173 * </p> 174 * <p> 175 * Violation Message Keys: 176 * </p> 177 * <ul> 178 * <li> 179 * {@code ncss.class} 180 * </li> 181 * <li> 182 * {@code ncss.file} 183 * </li> 184 * <li> 185 * {@code ncss.method} 186 * </li> 187 * <li> 188 * {@code ncss.record} 189 * </li> 190 * </ul> 191 * 192 * @since 3.5 193 */ 194// -@cs[AbbreviationAsWordInName] We can not change it as, 195// check's name is a part of API (used in configurations). 196@FileStatefulCheck 197public class JavaNCSSCheck extends AbstractCheck { 198 199 /** 200 * A key is pointing to the warning message text in "messages.properties" 201 * file. 202 */ 203 public static final String MSG_METHOD = "ncss.method"; 204 205 /** 206 * A key is pointing to the warning message text in "messages.properties" 207 * file. 208 */ 209 public static final String MSG_CLASS = "ncss.class"; 210 211 /** 212 * A key is pointing to the warning message text in "messages.properties" 213 * file. 214 */ 215 public static final String MSG_RECORD = "ncss.record"; 216 217 /** 218 * A key is pointing to the warning message text in "messages.properties" 219 * file. 220 */ 221 public static final String MSG_FILE = "ncss.file"; 222 223 /** Default constant for max file ncss. */ 224 private static final int FILE_MAX_NCSS = 2000; 225 226 /** Default constant for max file ncss. */ 227 private static final int CLASS_MAX_NCSS = 1500; 228 229 /** Default constant for max record ncss. */ 230 private static final int RECORD_MAX_NCSS = 150; 231 232 /** Default constant for max method ncss. */ 233 private static final int METHOD_MAX_NCSS = 50; 234 235 /** 236 * Specify the maximum allowed number of non commenting lines in a file 237 * including all top level and nested classes. 238 */ 239 private int fileMaximum = FILE_MAX_NCSS; 240 241 /** Specify the maximum allowed number of non commenting lines in a class. */ 242 private int classMaximum = CLASS_MAX_NCSS; 243 244 /** Specify the maximum allowed number of non commenting lines in a record. */ 245 private int recordMaximum = RECORD_MAX_NCSS; 246 247 /** Specify the maximum allowed number of non commenting lines in a method. */ 248 private int methodMaximum = METHOD_MAX_NCSS; 249 250 /** List containing the stacked counters. */ 251 private Deque<Counter> counters; 252 253 @Override 254 public int[] getDefaultTokens() { 255 return getRequiredTokens(); 256 } 257 258 @Override 259 public int[] getRequiredTokens() { 260 return new int[] { 261 TokenTypes.CLASS_DEF, 262 TokenTypes.INTERFACE_DEF, 263 TokenTypes.METHOD_DEF, 264 TokenTypes.CTOR_DEF, 265 TokenTypes.INSTANCE_INIT, 266 TokenTypes.STATIC_INIT, 267 TokenTypes.PACKAGE_DEF, 268 TokenTypes.IMPORT, 269 TokenTypes.VARIABLE_DEF, 270 TokenTypes.CTOR_CALL, 271 TokenTypes.SUPER_CTOR_CALL, 272 TokenTypes.LITERAL_IF, 273 TokenTypes.LITERAL_ELSE, 274 TokenTypes.LITERAL_WHILE, 275 TokenTypes.LITERAL_DO, 276 TokenTypes.LITERAL_FOR, 277 TokenTypes.LITERAL_SWITCH, 278 TokenTypes.LITERAL_BREAK, 279 TokenTypes.LITERAL_CONTINUE, 280 TokenTypes.LITERAL_RETURN, 281 TokenTypes.LITERAL_THROW, 282 TokenTypes.LITERAL_SYNCHRONIZED, 283 TokenTypes.LITERAL_CATCH, 284 TokenTypes.LITERAL_FINALLY, 285 TokenTypes.EXPR, 286 TokenTypes.LABELED_STAT, 287 TokenTypes.LITERAL_CASE, 288 TokenTypes.LITERAL_DEFAULT, 289 TokenTypes.RECORD_DEF, 290 TokenTypes.COMPACT_CTOR_DEF, 291 }; 292 } 293 294 @Override 295 public int[] getAcceptableTokens() { 296 return getRequiredTokens(); 297 } 298 299 @Override 300 public void beginTree(DetailAST rootAST) { 301 counters = new ArrayDeque<>(); 302 303 // add a counter for the file 304 counters.push(new Counter()); 305 } 306 307 @Override 308 public void visitToken(DetailAST ast) { 309 final int tokenType = ast.getType(); 310 311 if (tokenType == TokenTypes.CLASS_DEF 312 || tokenType == TokenTypes.RECORD_DEF 313 || isMethodOrCtorOrInitDefinition(tokenType)) { 314 // add a counter for this class/method 315 counters.push(new Counter()); 316 } 317 318 // check if token is countable 319 if (isCountable(ast)) { 320 // increment the stacked counters 321 counters.forEach(Counter::increment); 322 } 323 } 324 325 @Override 326 public void leaveToken(DetailAST ast) { 327 final int tokenType = ast.getType(); 328 329 if (isMethodOrCtorOrInitDefinition(tokenType)) { 330 // pop counter from the stack 331 final Counter counter = counters.pop(); 332 333 final int count = counter.getCount(); 334 if (count > methodMaximum) { 335 log(ast, MSG_METHOD, count, methodMaximum); 336 } 337 } 338 else if (tokenType == TokenTypes.CLASS_DEF) { 339 // pop counter from the stack 340 final Counter counter = counters.pop(); 341 342 final int count = counter.getCount(); 343 if (count > classMaximum) { 344 log(ast, MSG_CLASS, count, classMaximum); 345 } 346 } 347 else if (tokenType == TokenTypes.RECORD_DEF) { 348 // pop counter from the stack 349 final Counter counter = counters.pop(); 350 351 final int count = counter.getCount(); 352 if (count > recordMaximum) { 353 log(ast, MSG_RECORD, count, recordMaximum); 354 } 355 } 356 } 357 358 @Override 359 public void finishTree(DetailAST rootAST) { 360 // pop counter from the stack 361 final Counter counter = counters.pop(); 362 363 final int count = counter.getCount(); 364 if (count > fileMaximum) { 365 log(rootAST, MSG_FILE, count, fileMaximum); 366 } 367 } 368 369 /** 370 * Setter to specify the maximum allowed number of non commenting lines 371 * in a file including all top level and nested classes. 372 * 373 * @param fileMaximum 374 * the maximum ncss 375 */ 376 public void setFileMaximum(int fileMaximum) { 377 this.fileMaximum = fileMaximum; 378 } 379 380 /** 381 * Setter to specify the maximum allowed number of non commenting lines in a class. 382 * 383 * @param classMaximum 384 * the maximum ncss 385 */ 386 public void setClassMaximum(int classMaximum) { 387 this.classMaximum = classMaximum; 388 } 389 390 /** 391 * Setter to specify the maximum allowed number of non commenting lines in a record. 392 * 393 * @param recordMaximum 394 * the maximum ncss 395 */ 396 public void setRecordMaximum(int recordMaximum) { 397 this.recordMaximum = recordMaximum; 398 } 399 400 /** 401 * Setter to specify the maximum allowed number of non commenting lines in a method. 402 * 403 * @param methodMaximum 404 * the maximum ncss 405 */ 406 public void setMethodMaximum(int methodMaximum) { 407 this.methodMaximum = methodMaximum; 408 } 409 410 /** 411 * Checks if a token is countable for the ncss metric. 412 * 413 * @param ast 414 * the AST 415 * @return true if the token is countable 416 */ 417 private static boolean isCountable(DetailAST ast) { 418 boolean countable = true; 419 420 final int tokenType = ast.getType(); 421 422 // check if an expression is countable 423 if (tokenType == TokenTypes.EXPR) { 424 countable = isExpressionCountable(ast); 425 } 426 // check if a variable definition is countable 427 else if (tokenType == TokenTypes.VARIABLE_DEF) { 428 countable = isVariableDefCountable(ast); 429 } 430 return countable; 431 } 432 433 /** 434 * Checks if a variable definition is countable. 435 * 436 * @param ast the AST 437 * @return true if the variable definition is countable, false otherwise 438 */ 439 private static boolean isVariableDefCountable(DetailAST ast) { 440 boolean countable = false; 441 442 // count variable definitions only if they are direct child to a slist or 443 // object block 444 final int parentType = ast.getParent().getType(); 445 446 if (parentType == TokenTypes.SLIST 447 || parentType == TokenTypes.OBJBLOCK) { 448 final DetailAST prevSibling = ast.getPreviousSibling(); 449 450 // is countable if no previous sibling is found or 451 // the sibling is no COMMA. 452 // This is done because multiple assignment on one line are counted 453 // as 1 454 countable = prevSibling == null 455 || prevSibling.getType() != TokenTypes.COMMA; 456 } 457 458 return countable; 459 } 460 461 /** 462 * Checks if an expression is countable for the ncss metric. 463 * 464 * @param ast the AST 465 * @return true if the expression is countable, false otherwise 466 */ 467 private static boolean isExpressionCountable(DetailAST ast) { 468 final boolean countable; 469 470 // count expressions only if they are direct child to a slist (method 471 // body, for loop...) 472 // or direct child of label,if,else,do,while,for 473 final int parentType = ast.getParent().getType(); 474 switch (parentType) { 475 case TokenTypes.SLIST: 476 case TokenTypes.LABELED_STAT: 477 case TokenTypes.LITERAL_FOR: 478 case TokenTypes.LITERAL_DO: 479 case TokenTypes.LITERAL_WHILE: 480 case TokenTypes.LITERAL_IF: 481 case TokenTypes.LITERAL_ELSE: 482 // don't count if or loop conditions 483 final DetailAST prevSibling = ast.getPreviousSibling(); 484 countable = prevSibling == null 485 || prevSibling.getType() != TokenTypes.LPAREN; 486 break; 487 default: 488 countable = false; 489 break; 490 } 491 return countable; 492 } 493 494 /** 495 * Checks if a token is a method, constructor, or compact constructor definition. 496 * 497 * @param tokenType the type of token we are checking 498 * @return true if token type is method or ctor definition, false otherwise 499 */ 500 private static boolean isMethodOrCtorOrInitDefinition(int tokenType) { 501 return tokenType == TokenTypes.METHOD_DEF 502 || tokenType == TokenTypes.COMPACT_CTOR_DEF 503 || tokenType == TokenTypes.CTOR_DEF 504 || tokenType == TokenTypes.STATIC_INIT 505 || tokenType == TokenTypes.INSTANCE_INIT; 506 } 507 508 /** 509 * Class representing a counter. 510 * 511 */ 512 private static class Counter { 513 514 /** The counters internal integer. */ 515 private int count; 516 517 /** 518 * Increments the counter. 519 */ 520 public void increment() { 521 count++; 522 } 523 524 /** 525 * Gets the counters value. 526 * 527 * @return the counter 528 */ 529 public int getCount() { 530 return count; 531 } 532 533 } 534 535}