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 * Default value is {@code 50}. 060 * </li> 061 * <li> 062 * Property {@code classMaximum} - Specify the maximum allowed number of 063 * non commenting lines in a class. 064 * Default value is {@code 1500}. 065 * </li> 066 * <li> 067 * Property {@code fileMaximum} - Specify the maximum allowed number of 068 * non commenting lines in a file including all top level and nested classes. 069 * Default value is {@code 2000}. 070 * </li> 071 * </ul> 072 * <p> 073 * To configure the check: 074 * </p> 075 * <pre> 076 * <module name="JavaNCSS"/> 077 * </pre> 078 * <p>Example:</p> 079 * <pre> 080 * public void test() { 081 * System.out.println("Line 1"); 082 * // another 48 lines of code 083 * System.out.println("Line 50") // OK 084 * System.out.println("Line 51") // violation, the method crosses 50 non commented lines 085 * } 086 * </pre> 087 * <p> 088 * To configure the check with 40 allowed non commented lines for a method: 089 * </p> 090 * <pre> 091 * <module name="JavaNCSS"> 092 * <property name="methodMaximum" value="40"/> 093 * </module> 094 * </pre> 095 * <p>Example:</p> 096 * <pre> 097 * public void test() { 098 * System.out.println("Line 1"); 099 * // another 38 lines of code 100 * System.out.println("Line 40") // OK 101 * System.out.println("Line 41") // violation, the method crosses 40 non commented lines 102 * } 103 * </pre> 104 * <p> 105 * To configure the check to set limit of non commented lines in class to 100: 106 * </p> 107 * <pre> 108 * <module name="JavaNCSS"> 109 * <property name="classMaximum" value="100"/> 110 * </module> 111 * </pre> 112 * <p>Example:</p> 113 * <pre> 114 * public class Test { 115 * public void test() { 116 * System.out.println("Line 1"); 117 * // another 47 lines of code 118 * System.out.println("Line 49"); 119 * } 120 * 121 * public void test1() { 122 * System.out.println("Line 50"); // OK 123 * // another 47 lines of code 124 * System.out.println("Line 98"); // violation 125 * } 126 * } 127 * </pre> 128 * <p> 129 * To configure the check to set limit of non commented lines in file to 200: 130 * </p> 131 * <pre> 132 * <module name="JavaNCSS"> 133 * <property name="fileMaximum" value="200"/> 134 * </module> 135 * </pre> 136 * <p>Example:</p> 137 * <pre> 138 * public class Test1 { 139 * public void test() { 140 * System.out.println("Line 1"); 141 * // another 48 lines of code 142 * System.out.println("Line 49"); 143 * } 144 * 145 * public void test1() { 146 * System.out.println("Line 50"); 147 * // another 47 lines of code 148 * System.out.println("Line 98"); // OK 149 * } 150 * } 151 * 152 * class Test2 { 153 * public void test() { 154 * System.out.println("Line 150"); // OK 155 * } 156 * 157 * public void test1() { 158 * System.out.println("Line 200"); // violation 159 * } 160 * } 161 * </pre> 162 * 163 * @since 3.5 164 */ 165// -@cs[AbbreviationAsWordInName] We can not change it as, 166// check's name is a part of API (used in configurations). 167@FileStatefulCheck 168public class JavaNCSSCheck extends AbstractCheck { 169 170 /** 171 * A key is pointing to the warning message text in "messages.properties" 172 * file. 173 */ 174 public static final String MSG_METHOD = "ncss.method"; 175 176 /** 177 * A key is pointing to the warning message text in "messages.properties" 178 * file. 179 */ 180 public static final String MSG_CLASS = "ncss.class"; 181 182 /** 183 * A key is pointing to the warning message text in "messages.properties" 184 * file. 185 */ 186 public static final String MSG_FILE = "ncss.file"; 187 188 /** Default constant for max file ncss. */ 189 private static final int FILE_MAX_NCSS = 2000; 190 191 /** Default constant for max file ncss. */ 192 private static final int CLASS_MAX_NCSS = 1500; 193 194 /** Default constant for max method ncss. */ 195 private static final int METHOD_MAX_NCSS = 50; 196 197 /** 198 * Specify the maximum allowed number of non commenting lines in a file 199 * including all top level and nested classes. 200 */ 201 private int fileMaximum = FILE_MAX_NCSS; 202 203 /** Specify the maximum allowed number of non commenting lines in a class. */ 204 private int classMaximum = CLASS_MAX_NCSS; 205 206 /** Specify the maximum allowed number of non commenting lines in a method. */ 207 private int methodMaximum = METHOD_MAX_NCSS; 208 209 /** List containing the stacked counters. */ 210 private Deque<Counter> counters; 211 212 @Override 213 public int[] getDefaultTokens() { 214 return getRequiredTokens(); 215 } 216 217 @Override 218 public int[] getRequiredTokens() { 219 return new int[] { 220 TokenTypes.CLASS_DEF, 221 TokenTypes.INTERFACE_DEF, 222 TokenTypes.METHOD_DEF, 223 TokenTypes.CTOR_DEF, 224 TokenTypes.INSTANCE_INIT, 225 TokenTypes.STATIC_INIT, 226 TokenTypes.PACKAGE_DEF, 227 TokenTypes.IMPORT, 228 TokenTypes.VARIABLE_DEF, 229 TokenTypes.CTOR_CALL, 230 TokenTypes.SUPER_CTOR_CALL, 231 TokenTypes.LITERAL_IF, 232 TokenTypes.LITERAL_ELSE, 233 TokenTypes.LITERAL_WHILE, 234 TokenTypes.LITERAL_DO, 235 TokenTypes.LITERAL_FOR, 236 TokenTypes.LITERAL_SWITCH, 237 TokenTypes.LITERAL_BREAK, 238 TokenTypes.LITERAL_CONTINUE, 239 TokenTypes.LITERAL_RETURN, 240 TokenTypes.LITERAL_THROW, 241 TokenTypes.LITERAL_SYNCHRONIZED, 242 TokenTypes.LITERAL_CATCH, 243 TokenTypes.LITERAL_FINALLY, 244 TokenTypes.EXPR, 245 TokenTypes.LABELED_STAT, 246 TokenTypes.LITERAL_CASE, 247 TokenTypes.LITERAL_DEFAULT, 248 }; 249 } 250 251 @Override 252 public int[] getAcceptableTokens() { 253 return getRequiredTokens(); 254 } 255 256 @Override 257 public void beginTree(DetailAST rootAST) { 258 counters = new ArrayDeque<>(); 259 260 // add a counter for the file 261 counters.push(new Counter()); 262 } 263 264 @Override 265 public void visitToken(DetailAST ast) { 266 final int tokenType = ast.getType(); 267 268 if (tokenType == TokenTypes.CLASS_DEF 269 || tokenType == TokenTypes.METHOD_DEF 270 || tokenType == TokenTypes.CTOR_DEF 271 || tokenType == TokenTypes.STATIC_INIT 272 || tokenType == TokenTypes.INSTANCE_INIT) { 273 // add a counter for this class/method 274 counters.push(new Counter()); 275 } 276 277 // check if token is countable 278 if (isCountable(ast)) { 279 // increment the stacked counters 280 counters.forEach(Counter::increment); 281 } 282 } 283 284 @Override 285 public void leaveToken(DetailAST ast) { 286 final int tokenType = ast.getType(); 287 if (tokenType == TokenTypes.METHOD_DEF 288 || tokenType == TokenTypes.CTOR_DEF 289 || tokenType == TokenTypes.STATIC_INIT 290 || tokenType == TokenTypes.INSTANCE_INIT) { 291 // pop counter from the stack 292 final Counter counter = counters.pop(); 293 294 final int count = counter.getCount(); 295 if (count > methodMaximum) { 296 log(ast, MSG_METHOD, count, methodMaximum); 297 } 298 } 299 else if (tokenType == TokenTypes.CLASS_DEF) { 300 // pop counter from the stack 301 final Counter counter = counters.pop(); 302 303 final int count = counter.getCount(); 304 if (count > classMaximum) { 305 log(ast, MSG_CLASS, count, classMaximum); 306 } 307 } 308 } 309 310 @Override 311 public void finishTree(DetailAST rootAST) { 312 // pop counter from the stack 313 final Counter counter = counters.pop(); 314 315 final int count = counter.getCount(); 316 if (count > fileMaximum) { 317 log(rootAST, MSG_FILE, count, fileMaximum); 318 } 319 } 320 321 /** 322 * Setter to specify the maximum allowed number of non commenting lines 323 * in a file including all top level and nested classes. 324 * 325 * @param fileMaximum 326 * the maximum ncss 327 */ 328 public void setFileMaximum(int fileMaximum) { 329 this.fileMaximum = fileMaximum; 330 } 331 332 /** 333 * Setter to specify the maximum allowed number of non commenting lines in a class. 334 * 335 * @param classMaximum 336 * the maximum ncss 337 */ 338 public void setClassMaximum(int classMaximum) { 339 this.classMaximum = classMaximum; 340 } 341 342 /** 343 * Setter to specify the maximum allowed number of non commenting lines in a method. 344 * 345 * @param methodMaximum 346 * the maximum ncss 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 an 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 final boolean countable; 411 412 // count expressions only if they are direct child to a slist (method 413 // body, for loop...) 414 // or direct child of label,if,else,do,while,for 415 final int parentType = ast.getParent().getType(); 416 switch (parentType) { 417 case TokenTypes.SLIST: 418 case TokenTypes.LABELED_STAT: 419 case TokenTypes.LITERAL_FOR: 420 case TokenTypes.LITERAL_DO: 421 case TokenTypes.LITERAL_WHILE: 422 case TokenTypes.LITERAL_IF: 423 case TokenTypes.LITERAL_ELSE: 424 // don't count if or loop conditions 425 final DetailAST prevSibling = ast.getPreviousSibling(); 426 countable = prevSibling == null 427 || prevSibling.getType() != TokenTypes.LPAREN; 428 break; 429 default: 430 countable = false; 431 break; 432 } 433 return countable; 434 } 435 436 /** 437 * Class representing a counter. 438 * 439 */ 440 private static class Counter { 441 442 /** The counters internal integer. */ 443 private int count; 444 445 /** 446 * Increments the counter. 447 */ 448 public void increment() { 449 count++; 450 } 451 452 /** 453 * Gets the counters value. 454 * 455 * @return the counter 456 */ 457 public int getCount() { 458 return count; 459 } 460 461 } 462 463}