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.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; 030 031/** 032 * <p> 033 * Checks cyclomatic complexity against a specified limit. It is a measure of 034 * the minimum number of possible paths through the source and therefore the 035 * number of required tests, it is not a about quality of code! It is only 036 * applied to methods, c-tors, 037 * <a href="https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html"> 038 * static initializers and instance initializers</a>. 039 * </p> 040 * <p> 041 * The complexity is equal to the number of decision points {@code + 1}. 042 * Decision points: {@code if}, {@code while}, {@code do}, {@code for}, 043 * {@code ?:}, {@code catch}, {@code switch}, {@code case} statements and 044 * operators {@code &&} and {@code ||} in the body of target. 045 * </p> 046 * <p> 047 * By pure theory level 1-4 is considered easy to test, 5-7 OK, 8-10 consider 048 * re-factoring to ease testing, and 11+ re-factor now as testing will be painful. 049 * </p> 050 * <p> 051 * When it comes to code quality measurement by this metric level 10 is very 052 * good level as a ultimate target (that is hard to archive). Do not be ashamed 053 * to have complexity level 15 or even higher, but keep it below 20 to catch 054 * really bad designed code automatically. 055 * </p> 056 * <p> 057 * Please use Suppression to avoid violations on cases that could not be split 058 * in few methods without damaging readability of code or encapsulation. 059 * </p> 060 * <ul> 061 * <li> 062 * Property {@code max} - Specify the maximum threshold allowed. 063 * Default value is {@code 10}. 064 * </li> 065 * <li> 066 * Property {@code switchBlockAsSingleDecisionPoint} - Control whether to treat 067 * the whole switch block as a single decision point. 068 * Default value is {@code false}. 069 * </li> 070 * <li> 071 * Property {@code tokens} - tokens to check 072 * Default value is: 073 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_WHILE"> 074 * LITERAL_WHILE</a>, 075 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_DO"> 076 * LITERAL_DO</a>, 077 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_FOR"> 078 * LITERAL_FOR</a>, 079 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_IF"> 080 * LITERAL_IF</a>, 081 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_SWITCH"> 082 * LITERAL_SWITCH</a>, 083 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_CASE"> 084 * LITERAL_CASE</a>, 085 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_CATCH"> 086 * LITERAL_CATCH</a>, 087 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#QUESTION"> 088 * QUESTION</a>, 089 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LAND"> 090 * LAND</a>, 091 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LOR"> 092 * LOR</a>. 093 * </li> 094 * </ul> 095 * <p> 096 * To configure the check: 097 * </p> 098 * <pre> 099 * <module name="CyclomaticComplexity"/> 100 * </pre> 101 * <p> 102 * Example: 103 * </p> 104 * <pre> 105 * class CyclomaticComplexity { 106 * // Cyclomatic Complexity = 11 107 * int a, b, c, d, n; 108 * public void foo() { // 1, function declaration 109 * if (a == 1) { // 2, if 110 * fun1(); 111 * } else if (a == b // 3, if 112 * && a == c) { // 4, && operator 113 * if (c == 2) { // 5, if 114 * fun2(); 115 * } 116 * } else if (a == d) { // 6, if 117 * try { 118 * fun4(); 119 * } catch (Exception e) { // 7, catch 120 * } 121 * } else { 122 * switch(n) { 123 * case 1: // 8, case 124 * fun1(); 125 * break; 126 * case 2: // 9, case 127 * fun2(); 128 * break; 129 * case 3: // 10, case 130 * fun3(); 131 * break; 132 * default: 133 * break; 134 * } 135 * } 136 * d = a < 0 ? -1 : 1; // 11, ternary operator 137 * } 138 * } 139 * </pre> 140 * <p> 141 * To configure the check with a threshold of 4 and check only for while and do-while loops: 142 * </p> 143 * <pre> 144 * <module name="CyclomaticComplexity"> 145 * <property name="max" value="4"/> 146 * <property name="tokens" value="LITERAL_WHILE, LITERAL_DO"/> 147 * </module> 148 * </pre> 149 * <p> 150 * Example: 151 * </p> 152 * <pre> 153 * class CyclomaticComplexity { 154 * // Cyclomatic Complexity = 5 155 * int a, b, c, d; 156 * public void foo() { // 1, function declaration 157 * while (a < b // 2, while 158 * && a > c) { 159 * fun(); 160 * } 161 * if (a == b) { 162 * do { // 3, do 163 * fun(); 164 * } while (d); 165 * } else if (c == d) { 166 * while (c > 0) { // 4, while 167 * fun(); 168 * } 169 * do { // 5, do-while 170 * fun(); 171 * } while (a); 172 * } 173 * } 174 * } 175 * </pre> 176 * <p> 177 * To configure the check to consider switch-case block as one decision point. 178 * </p> 179 * <pre> 180 * <module name="CyclomaticComplexity"> 181 * <property name="switchBlockAsSingleDecisionPoint" value="true"/> 182 * </module> 183 * </pre> 184 * <p> 185 * Example: 186 * </p> 187 * <pre> 188 * class CyclomaticComplexity { 189 * // Cyclomatic Complexity = 11 190 * int a, b, c, d, e, n; 191 * public void foo() { // 1, function declaration 192 * if (a == b) { // 2, if 193 * fun1(); 194 * } else if (a == 0 // 3, if 195 * && b == c) { // 4, && operator 196 * if (c == -1) { // 5, if 197 * fun2(); 198 * } 199 * } else if (a == c // 6, if 200 * || a == d) { // 7, || operator 201 * fun3(); 202 * } else if (d == e) { // 8, if 203 * try { 204 * fun4(); 205 * } catch (Exception e) { // 9, catch 206 * } 207 * } else { 208 * switch(n) { // 10, switch 209 * case 1: 210 * fun1(); 211 * break; 212 * case 2: 213 * fun2(); 214 * break; 215 * default: 216 * break; 217 * } 218 * } 219 * a = a > 0 ? b : c; // 11, ternary operator 220 * } 221 * } 222 * </pre> 223 * 224 * @since 3.2 225 */ 226@FileStatefulCheck 227public class CyclomaticComplexityCheck 228 extends AbstractCheck { 229 230 /** 231 * A key is pointing to the warning message text in "messages.properties" 232 * file. 233 */ 234 public static final String MSG_KEY = "cyclomaticComplexity"; 235 236 /** The initial current value. */ 237 private static final BigInteger INITIAL_VALUE = BigInteger.ONE; 238 239 /** Default allowed complexity. */ 240 private static final int DEFAULT_COMPLEXITY_VALUE = 10; 241 242 /** Stack of values - all but the current value. */ 243 private final Deque<BigInteger> valueStack = new ArrayDeque<>(); 244 245 /** Control whether to treat the whole switch block as a single decision point. */ 246 private boolean switchBlockAsSingleDecisionPoint; 247 248 /** The current value. */ 249 private BigInteger currentValue = INITIAL_VALUE; 250 251 /** Specify the maximum threshold allowed. */ 252 private int max = DEFAULT_COMPLEXITY_VALUE; 253 254 /** 255 * Setter to control whether to treat the whole switch block as a single decision point. 256 * 257 * @param switchBlockAsSingleDecisionPoint whether to treat the whole switch 258 * block as a single decision point. 259 */ 260 public void setSwitchBlockAsSingleDecisionPoint(boolean switchBlockAsSingleDecisionPoint) { 261 this.switchBlockAsSingleDecisionPoint = switchBlockAsSingleDecisionPoint; 262 } 263 264 /** 265 * Setter to specify the maximum threshold allowed. 266 * 267 * @param max the maximum threshold 268 */ 269 public final void setMax(int max) { 270 this.max = max; 271 } 272 273 @Override 274 public int[] getDefaultTokens() { 275 return new int[] { 276 TokenTypes.CTOR_DEF, 277 TokenTypes.METHOD_DEF, 278 TokenTypes.INSTANCE_INIT, 279 TokenTypes.STATIC_INIT, 280 TokenTypes.LITERAL_WHILE, 281 TokenTypes.LITERAL_DO, 282 TokenTypes.LITERAL_FOR, 283 TokenTypes.LITERAL_IF, 284 TokenTypes.LITERAL_SWITCH, 285 TokenTypes.LITERAL_CASE, 286 TokenTypes.LITERAL_CATCH, 287 TokenTypes.QUESTION, 288 TokenTypes.LAND, 289 TokenTypes.LOR, 290 }; 291 } 292 293 @Override 294 public int[] getAcceptableTokens() { 295 return new int[] { 296 TokenTypes.CTOR_DEF, 297 TokenTypes.METHOD_DEF, 298 TokenTypes.INSTANCE_INIT, 299 TokenTypes.STATIC_INIT, 300 TokenTypes.LITERAL_WHILE, 301 TokenTypes.LITERAL_DO, 302 TokenTypes.LITERAL_FOR, 303 TokenTypes.LITERAL_IF, 304 TokenTypes.LITERAL_SWITCH, 305 TokenTypes.LITERAL_CASE, 306 TokenTypes.LITERAL_CATCH, 307 TokenTypes.QUESTION, 308 TokenTypes.LAND, 309 TokenTypes.LOR, 310 }; 311 } 312 313 @Override 314 public final int[] getRequiredTokens() { 315 return new int[] { 316 TokenTypes.CTOR_DEF, 317 TokenTypes.METHOD_DEF, 318 TokenTypes.INSTANCE_INIT, 319 TokenTypes.STATIC_INIT, 320 }; 321 } 322 323 @Override 324 public void visitToken(DetailAST ast) { 325 switch (ast.getType()) { 326 case TokenTypes.CTOR_DEF: 327 case TokenTypes.METHOD_DEF: 328 case TokenTypes.INSTANCE_INIT: 329 case TokenTypes.STATIC_INIT: 330 visitMethodDef(); 331 break; 332 default: 333 visitTokenHook(ast); 334 } 335 } 336 337 @Override 338 public void leaveToken(DetailAST ast) { 339 switch (ast.getType()) { 340 case TokenTypes.CTOR_DEF: 341 case TokenTypes.METHOD_DEF: 342 case TokenTypes.INSTANCE_INIT: 343 case TokenTypes.STATIC_INIT: 344 leaveMethodDef(ast); 345 break; 346 default: 347 break; 348 } 349 } 350 351 /** 352 * Hook called when visiting a token. Will not be called the method 353 * definition tokens. 354 * 355 * @param ast the token being visited 356 */ 357 private void visitTokenHook(DetailAST ast) { 358 if (switchBlockAsSingleDecisionPoint) { 359 if (ast.getType() != TokenTypes.LITERAL_CASE) { 360 incrementCurrentValue(BigInteger.ONE); 361 } 362 } 363 else if (ast.getType() != TokenTypes.LITERAL_SWITCH) { 364 incrementCurrentValue(BigInteger.ONE); 365 } 366 } 367 368 /** 369 * Process the end of a method definition. 370 * 371 * @param ast the token representing the method definition 372 */ 373 private void leaveMethodDef(DetailAST ast) { 374 final BigInteger bigIntegerMax = BigInteger.valueOf(max); 375 if (currentValue.compareTo(bigIntegerMax) > 0) { 376 log(ast, MSG_KEY, currentValue, bigIntegerMax); 377 } 378 popValue(); 379 } 380 381 /** 382 * Increments the current value by a specified amount. 383 * 384 * @param amount the amount to increment by 385 */ 386 private void incrementCurrentValue(BigInteger amount) { 387 currentValue = currentValue.add(amount); 388 } 389 390 /** Push the current value on the stack. */ 391 private void pushValue() { 392 valueStack.push(currentValue); 393 currentValue = INITIAL_VALUE; 394 } 395 396 /** 397 * Pops a value off the stack and makes it the current value. 398 */ 399 private void popValue() { 400 currentValue = valueStack.pop(); 401 } 402 403 /** Process the start of the method definition. */ 404 private void visitMethodDef() { 405 pushValue(); 406 } 407 408}