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