001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2025 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.math.BigInteger; 023import java.util.ArrayDeque; 024import java.util.Deque; 025 026import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 027import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 028import com.puppycrawl.tools.checkstyle.api.DetailAST; 029import com.puppycrawl.tools.checkstyle.api.TokenTypes; 030import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 031 032/** 033 * <div> 034 * Checks the NPATH complexity against a specified limit. 035 * </div> 036 * 037 * <p> 038 * The NPATH metric computes the number of possible execution paths through a 039 * function(method). It takes into account the nesting of conditional statements 040 * and multipart boolean expressions (A && B, C || D, E ? F :G and 041 * their combinations). 042 * </p> 043 * 044 * <p> 045 * The NPATH metric was designed base on Cyclomatic complexity to avoid problem 046 * of Cyclomatic complexity metric like nesting level within a function(method). 047 * </p> 048 * 049 * <p> 050 * Metric was described at <a href="http://dl.acm.org/citation.cfm?id=42379"> 051 * "NPATH: a measure of execution pathcomplexity and its applications"</a>. 052 * If you need detailed description of algorithm, please read that article, 053 * it is well written and have number of examples and details. 054 * </p> 055 * 056 * <p> 057 * Here is some quotes: 058 * </p> 059 * <blockquote> 060 * An NPATH threshold value of 200 has been established for a function. 061 * The value 200 is based on studies done at AT&T Bell Laboratories [1988 year]. 062 * </blockquote> 063 * <blockquote> 064 * Some of the most effective methods of reducing the NPATH value include: 065 * <ul> 066 * <li> 067 * distributing functionality; 068 * </li> 069 * <li> 070 * implementing multiple if statements as a switch statement; 071 * </li> 072 * <li> 073 * creating a separate function for logical expressions with a high count of 074 * variables and (&&) and or (||) operators. 075 * </li> 076 * </ul> 077 * </blockquote> 078 * <blockquote> 079 * Although strategies to reduce the NPATH complexity of functions are important, 080 * care must be taken not to distort the logical clarity of the software by 081 * applying a strategy to reduce the complexity of functions. That is, there is 082 * a point of diminishing return beyond which a further attempt at reduction of 083 * complexity distorts the logical clarity of the system structure. 084 * </blockquote> 085 * <div class="wrapper"> 086 * <table> 087 * <caption>Examples</caption> 088 * <thead><tr><th>Structure</th><th>Complexity expression</th></tr></thead> 089 * <tr><td>if ([expr]) { [if-range] }</td><td>NP(if-range) + 1 + NP(expr)</td></tr> 090 * <tr><td>if ([expr]) { [if-range] } else { [else-range] }</td> 091 * <td>NP(if-range)+ NP(else-range) + NP(expr)</td></tr> 092 * <tr><td>while ([expr]) { [while-range] }</td><td>NP(while-range) + NP(expr) + 1</td></tr> 093 * <tr><td>do { [do-range] } while ([expr])</td><td>NP(do-range) + NP(expr) + 1</td></tr> 094 * <tr><td>for([expr1]; [expr2]; [expr3]) { [for-range] }</td> 095 * <td>NP(for-range) + NP(expr1)+ NP(expr2) + NP(expr3) + 1</td></tr> 096 * <tr><td>switch ([expr]) { case : [case-range] default: [default-range] }</td> 097 * <td>S(i=1:i=n)NP(case-range[i]) + NP(default-range) + NP(expr)</td></tr> 098 * <tr><td>when[expr]</td><td>NP(expr) + 1</td></tr> 099 * <tr><td>[expr1] ? [expr2] : [expr3]</td><td>NP(expr1) + NP(expr2) + NP(expr3) + 2</td></tr> 100 * <tr><td>goto label</td><td>1</td></tr><tr><td>break</td><td>1</td></tr> 101 * <tr><td>Expressions</td> 102 * <td>Number of && and || operators in expression. No operators - 0</td></tr> 103 * <tr><td>continue</td><td>1</td></tr><tr><td>return</td><td>1</td></tr> 104 * <tr><td>Statement (even sequential statements)</td><td>1</td></tr> 105 * <tr><td>Empty block {}</td><td>1</td></tr><tr><td>Function call</td><td>1</td> 106 * </tr><tr><td>Function(Method) declaration or Block</td><td>P(i=1:i=N)NP(Statement[i])</td></tr> 107 * </table> 108 * </div> 109 * 110 * <p> 111 * <b>Rationale:</b> Nejmeh says that his group had an informal NPATH limit of 112 * 200 on individual routines; functions(methods) that exceeded this value were 113 * candidates for further decomposition - or at least a closer look. 114 * <b>Please do not be fanatic with limit 200</b> - choose number that suites 115 * your project style. Limit 200 is empirical number base on some sources of at 116 * AT&T Bell Laboratories of 1988 year. 117 * </p> 118 * 119 * @since 3.4 120 */ 121// -@cs[AbbreviationAsWordInName] Can't change check name 122@FileStatefulCheck 123public final class NPathComplexityCheck extends AbstractCheck { 124 125 /** 126 * A key is pointing to the warning message text in "messages.properties" 127 * file. 128 */ 129 public static final String MSG_KEY = "npathComplexity"; 130 131 /** Tokens that are considered as case labels. */ 132 private static final int[] CASE_LABEL_TOKENS = { 133 TokenTypes.EXPR, 134 TokenTypes.PATTERN_DEF, 135 TokenTypes.PATTERN_VARIABLE_DEF, 136 TokenTypes.RECORD_PATTERN_DEF, 137 }; 138 139 /** Default allowed complexity. */ 140 private static final int DEFAULT_MAX = 200; 141 142 /** The initial current value. */ 143 private static final BigInteger INITIAL_VALUE = BigInteger.ZERO; 144 145 /** 146 * Stack of NP values for ranges. 147 */ 148 private final Deque<BigInteger> rangeValues = new ArrayDeque<>(); 149 150 /** Stack of NP values for expressions. */ 151 private final Deque<Integer> expressionValues = new ArrayDeque<>(); 152 153 /** Stack of belongs to range values for question operator. */ 154 private final Deque<Boolean> afterValues = new ArrayDeque<>(); 155 156 /** 157 * Range of the last processed expression. Used for checking that ternary operation 158 * which is a part of expression won't be processed for second time. 159 */ 160 private final TokenEnd processingTokenEnd = new TokenEnd(); 161 162 /** NP value for current range. */ 163 private BigInteger currentRangeValue; 164 165 /** Specify the maximum threshold allowed. */ 166 private int max = DEFAULT_MAX; 167 168 /** True, when branch is visited, but not leaved. */ 169 private boolean branchVisited; 170 171 /** 172 * Setter to specify the maximum threshold allowed. 173 * 174 * @param max the maximum threshold 175 * @since 3.4 176 */ 177 public void setMax(int max) { 178 this.max = max; 179 } 180 181 @Override 182 public int[] getDefaultTokens() { 183 return getRequiredTokens(); 184 } 185 186 @Override 187 public int[] getAcceptableTokens() { 188 return getRequiredTokens(); 189 } 190 191 @Override 192 public int[] getRequiredTokens() { 193 return new int[] { 194 TokenTypes.CTOR_DEF, 195 TokenTypes.METHOD_DEF, 196 TokenTypes.STATIC_INIT, 197 TokenTypes.INSTANCE_INIT, 198 TokenTypes.LITERAL_WHILE, 199 TokenTypes.LITERAL_DO, 200 TokenTypes.LITERAL_FOR, 201 TokenTypes.LITERAL_IF, 202 TokenTypes.LITERAL_ELSE, 203 TokenTypes.LITERAL_SWITCH, 204 TokenTypes.CASE_GROUP, 205 TokenTypes.LITERAL_TRY, 206 TokenTypes.LITERAL_CATCH, 207 TokenTypes.QUESTION, 208 TokenTypes.LITERAL_RETURN, 209 TokenTypes.LITERAL_DEFAULT, 210 TokenTypes.COMPACT_CTOR_DEF, 211 TokenTypes.SWITCH_RULE, 212 TokenTypes.LITERAL_WHEN, 213 }; 214 } 215 216 @Override 217 public void beginTree(DetailAST rootAST) { 218 rangeValues.clear(); 219 expressionValues.clear(); 220 afterValues.clear(); 221 processingTokenEnd.reset(); 222 currentRangeValue = INITIAL_VALUE; 223 branchVisited = false; 224 } 225 226 @Override 227 public void visitToken(DetailAST ast) { 228 switch (ast.getType()) { 229 case TokenTypes.LITERAL_IF, TokenTypes.LITERAL_SWITCH, 230 TokenTypes.LITERAL_WHILE, TokenTypes.LITERAL_DO, 231 TokenTypes.LITERAL_FOR -> visitConditional(ast, 1); 232 233 case TokenTypes.QUESTION -> visitUnitaryOperator(ast, 2); 234 235 case TokenTypes.LITERAL_RETURN -> visitUnitaryOperator(ast, 0); 236 237 case TokenTypes.LITERAL_WHEN -> visitWhenExpression(ast, 1); 238 239 case TokenTypes.CASE_GROUP -> { 240 final int caseNumber = countCaseTokens(ast); 241 branchVisited = true; 242 pushValue(caseNumber); 243 } 244 245 case TokenTypes.SWITCH_RULE -> { 246 final int caseConstantNumber = countCaseConstants(ast); 247 branchVisited = true; 248 pushValue(caseConstantNumber); 249 } 250 251 case TokenTypes.LITERAL_ELSE -> { 252 branchVisited = true; 253 if (currentRangeValue.equals(BigInteger.ZERO)) { 254 currentRangeValue = BigInteger.ONE; 255 } 256 pushValue(0); 257 } 258 259 case TokenTypes.LITERAL_TRY, 260 TokenTypes.LITERAL_CATCH, 261 TokenTypes.LITERAL_DEFAULT -> pushValue(1); 262 263 case TokenTypes.CTOR_DEF, 264 TokenTypes.METHOD_DEF, 265 TokenTypes.INSTANCE_INIT, 266 TokenTypes.STATIC_INIT, 267 TokenTypes.COMPACT_CTOR_DEF -> pushValue(0); 268 269 default -> { 270 // do nothing 271 } 272 } 273 } 274 275 @Override 276 public void leaveToken(DetailAST ast) { 277 switch (ast.getType()) { 278 case TokenTypes.LITERAL_WHILE, 279 TokenTypes.LITERAL_DO, 280 TokenTypes.LITERAL_FOR, 281 TokenTypes.LITERAL_IF, 282 TokenTypes.LITERAL_SWITCH, 283 TokenTypes.LITERAL_WHEN -> leaveConditional(); 284 285 case TokenTypes.LITERAL_TRY -> leaveMultiplyingConditional(); 286 287 case TokenTypes.LITERAL_RETURN, 288 TokenTypes.QUESTION -> leaveUnitaryOperator(); 289 290 case TokenTypes.LITERAL_CATCH -> leaveAddingConditional(); 291 292 case TokenTypes.LITERAL_DEFAULT -> leaveBranch(); 293 294 case TokenTypes.LITERAL_ELSE, 295 TokenTypes.CASE_GROUP, 296 TokenTypes.SWITCH_RULE -> { 297 leaveBranch(); 298 branchVisited = false; 299 } 300 301 case TokenTypes.CTOR_DEF, 302 TokenTypes.METHOD_DEF, 303 TokenTypes.INSTANCE_INIT, 304 TokenTypes.STATIC_INIT, 305 TokenTypes.COMPACT_CTOR_DEF -> leaveMethodDef(ast); 306 307 default -> { 308 // do nothing 309 } 310 } 311 } 312 313 /** 314 * Visits if, while, do-while, for and switch tokens - all of them have expression in 315 * parentheses which is used for calculation. 316 * 317 * @param ast visited token. 318 * @param basicBranchingFactor default number of branches added. 319 */ 320 private void visitConditional(DetailAST ast, int basicBranchingFactor) { 321 int expressionValue = basicBranchingFactor; 322 DetailAST bracketed; 323 for (bracketed = ast.findFirstToken(TokenTypes.LPAREN); 324 bracketed.getType() != TokenTypes.RPAREN; 325 bracketed = bracketed.getNextSibling()) { 326 expressionValue += countConditionalOperators(bracketed); 327 } 328 processingTokenEnd.setToken(bracketed); 329 pushValue(expressionValue); 330 } 331 332 /** 333 * Visits when expression token. There is no guarantee that when expression will be 334 * bracketed, so we don't use visitConditional method. 335 * 336 * @param ast visited token. 337 * @param basicBranchingFactor default number of branches added. 338 */ 339 private void visitWhenExpression(DetailAST ast, int basicBranchingFactor) { 340 final int expressionValue = basicBranchingFactor + countConditionalOperators(ast); 341 processingTokenEnd.setToken(getLastToken(ast)); 342 pushValue(expressionValue); 343 } 344 345 /** 346 * Visits ternary operator (?:) and return tokens. They differ from those processed by 347 * visitConditional method in that their expression isn't bracketed. 348 * 349 * @param ast visited token. 350 * @param basicBranchingFactor number of branches inherently added by this token. 351 */ 352 private void visitUnitaryOperator(DetailAST ast, int basicBranchingFactor) { 353 final boolean isAfter = processingTokenEnd.isAfter(ast); 354 afterValues.push(isAfter); 355 if (!isAfter) { 356 processingTokenEnd.setToken(getLastToken(ast)); 357 final int expressionValue = basicBranchingFactor + countConditionalOperators(ast); 358 pushValue(expressionValue); 359 } 360 } 361 362 /** 363 * Leaves ternary operator (?:) and return tokens. 364 */ 365 private void leaveUnitaryOperator() { 366 if (Boolean.FALSE.equals(afterValues.pop())) { 367 final Values valuePair = popValue(); 368 BigInteger basicRangeValue = valuePair.getRangeValue(); 369 BigInteger expressionValue = valuePair.getExpressionValue(); 370 if (expressionValue.equals(BigInteger.ZERO)) { 371 expressionValue = BigInteger.ONE; 372 } 373 if (basicRangeValue.equals(BigInteger.ZERO)) { 374 basicRangeValue = BigInteger.ONE; 375 } 376 currentRangeValue = currentRangeValue.add(expressionValue).multiply(basicRangeValue); 377 } 378 } 379 380 /** Leaves while, do, for, if, ternary (?::), return or switch. */ 381 private void leaveConditional() { 382 final Values valuePair = popValue(); 383 final BigInteger expressionValue = valuePair.getExpressionValue(); 384 BigInteger basicRangeValue = valuePair.getRangeValue(); 385 if (currentRangeValue.equals(BigInteger.ZERO)) { 386 currentRangeValue = BigInteger.ONE; 387 } 388 if (basicRangeValue.equals(BigInteger.ZERO)) { 389 basicRangeValue = BigInteger.ONE; 390 } 391 currentRangeValue = currentRangeValue.add(expressionValue).multiply(basicRangeValue); 392 } 393 394 /** Leaves else, default or case group tokens. */ 395 private void leaveBranch() { 396 final Values valuePair = popValue(); 397 final BigInteger basicRangeValue = valuePair.getRangeValue(); 398 final BigInteger expressionValue = valuePair.getExpressionValue(); 399 if (branchVisited && currentRangeValue.equals(BigInteger.ZERO)) { 400 currentRangeValue = BigInteger.ONE; 401 } 402 currentRangeValue = currentRangeValue.subtract(BigInteger.ONE) 403 .add(basicRangeValue) 404 .add(expressionValue); 405 } 406 407 /** 408 * Process the end of a method definition. 409 * 410 * @param ast the token type representing the method definition 411 */ 412 private void leaveMethodDef(DetailAST ast) { 413 final BigInteger bigIntegerMax = BigInteger.valueOf(max); 414 if (currentRangeValue.compareTo(bigIntegerMax) > 0) { 415 log(ast, MSG_KEY, currentRangeValue, bigIntegerMax); 416 } 417 popValue(); 418 currentRangeValue = INITIAL_VALUE; 419 } 420 421 /** Leaves catch. */ 422 private void leaveAddingConditional() { 423 currentRangeValue = currentRangeValue.add(popValue().getRangeValue().add(BigInteger.ONE)); 424 } 425 426 /** 427 * Pushes the current range value on the range value stack. Pushes this token expression value 428 * on the expression value stack. 429 * 430 * @param expressionValue value of expression calculated for current token. 431 */ 432 private void pushValue(Integer expressionValue) { 433 rangeValues.push(currentRangeValue); 434 expressionValues.push(expressionValue); 435 currentRangeValue = INITIAL_VALUE; 436 } 437 438 /** 439 * Pops values from both stack of expression values and stack of range values. 440 * 441 * @return pair of head values from both of the stacks. 442 */ 443 private Values popValue() { 444 final int expressionValue = expressionValues.pop(); 445 return new Values(rangeValues.pop(), BigInteger.valueOf(expressionValue)); 446 } 447 448 /** Leaves try. */ 449 private void leaveMultiplyingConditional() { 450 currentRangeValue = currentRangeValue.add(BigInteger.ONE) 451 .multiply(popValue().getRangeValue().add(BigInteger.ONE)); 452 } 453 454 /** 455 * Calculates number of conditional operators, including inline ternary operator, for a token. 456 * 457 * @param ast inspected token. 458 * @return number of conditional operators. 459 * @see <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.23"> 460 * Java Language Specification, §15.23</a> 461 * @see <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.24"> 462 * Java Language Specification, §15.24</a> 463 * @see <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.25"> 464 * Java Language Specification, §15.25</a> 465 */ 466 private static int countConditionalOperators(DetailAST ast) { 467 int number = 0; 468 for (DetailAST child = ast.getFirstChild(); child != null; 469 child = child.getNextSibling()) { 470 final int type = child.getType(); 471 if (type == TokenTypes.LOR || type == TokenTypes.LAND) { 472 number++; 473 } 474 else if (type == TokenTypes.QUESTION) { 475 number += 2; 476 } 477 number += countConditionalOperators(child); 478 } 479 return number; 480 } 481 482 /** 483 * Finds a leaf, which is the most distant from the root. 484 * 485 * @param ast the root of tree. 486 * @return the leaf. 487 */ 488 private static DetailAST getLastToken(DetailAST ast) { 489 final DetailAST lastChild = ast.getLastChild(); 490 final DetailAST result; 491 if (lastChild.getFirstChild() == null) { 492 result = lastChild; 493 } 494 else { 495 result = getLastToken(lastChild); 496 } 497 return result; 498 } 499 500 /** 501 * Counts number of case tokens subject to a case group token. 502 * 503 * @param ast case group token. 504 * @return number of case tokens. 505 */ 506 private static int countCaseTokens(DetailAST ast) { 507 int counter = 0; 508 for (DetailAST iterator = ast.getFirstChild(); iterator != null; 509 iterator = iterator.getNextSibling()) { 510 if (iterator.getType() == TokenTypes.LITERAL_CASE) { 511 counter++; 512 } 513 } 514 return counter; 515 } 516 517 /** 518 * Counts number of case constants tokens in a switch labeled rule. 519 * 520 * @param ast switch rule token. 521 * @return number of case constant tokens. 522 */ 523 private static int countCaseConstants(DetailAST ast) { 524 int counter = 0; 525 final DetailAST literalCase = ast.getFirstChild(); 526 527 for (DetailAST node = literalCase.getFirstChild(); node != null; 528 node = node.getNextSibling()) { 529 if (TokenUtil.isOfType(node, CASE_LABEL_TOKENS)) { 530 counter++; 531 } 532 } 533 534 return counter; 535 } 536 537 /** 538 * Coordinates of token end. Used to prevent inline ternary 539 * operator from being processed twice. 540 */ 541 private static final class TokenEnd { 542 543 /** End line of token. */ 544 private int endLineNo; 545 546 /** End column of token. */ 547 private int endColumnNo; 548 549 /** 550 * Sets end coordinates from given token. 551 * 552 * @param endToken token. 553 */ 554 public void setToken(DetailAST endToken) { 555 if (!isAfter(endToken)) { 556 endLineNo = endToken.getLineNo(); 557 endColumnNo = endToken.getColumnNo(); 558 } 559 } 560 561 /** Sets end token coordinates to the start of the file. */ 562 public void reset() { 563 endLineNo = 0; 564 endColumnNo = 0; 565 } 566 567 /** 568 * Checks if saved coordinates located after given token. 569 * 570 * @param ast given token. 571 * @return true, if saved coordinates located after given token. 572 */ 573 public boolean isAfter(DetailAST ast) { 574 final int lineNo = ast.getLineNo(); 575 final int columnNo = ast.getColumnNo(); 576 return lineNo <= endLineNo 577 && (lineNo != endLineNo 578 || columnNo <= endColumnNo); 579 } 580 581 } 582 583 /** 584 * Class that store range value and expression value. 585 */ 586 private static final class Values { 587 588 /** NP value for range. */ 589 private final BigInteger rangeValue; 590 591 /** NP value for expression. */ 592 private final BigInteger expressionValue; 593 594 /** 595 * Constructor that assigns all of class fields. 596 * 597 * @param valueOfRange NP value for range 598 * @param valueOfExpression NP value for expression 599 */ 600 private Values(BigInteger valueOfRange, BigInteger valueOfExpression) { 601 rangeValue = valueOfRange; 602 expressionValue = valueOfExpression; 603 } 604 605 /** 606 * Returns NP value for range. 607 * 608 * @return NP value for range 609 */ 610 public BigInteger getRangeValue() { 611 return rangeValue; 612 } 613 614 /** 615 * Returns NP value for expression. 616 * 617 * @return NP value for expression 618 */ 619 public BigInteger getExpressionValue() { 620 return expressionValue; 621 } 622 623 } 624 625}