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