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