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.ScopeUtil; 031 032/** 033 * <div> 034 * Checks cyclomatic complexity against a specified limit. It is a measure of 035 * the minimum number of possible paths through the source and therefore the 036 * number of required tests, it is not about quality of code! It is only 037 * applied to methods, c-tors, 038 * <a href="https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html"> 039 * static initializers and instance initializers</a>. 040 * </div> 041 * 042 * <p> 043 * The complexity is equal to the number of decision points {@code + 1}. 044 * Decision points: 045 * </p> 046 * <ul> 047 * <li> 048 * {@code if}, {@code while}, {@code do}, {@code for}, 049 * {@code ?:}, {@code catch}, {@code switch}, {@code case} statements. 050 * </li> 051 * <li> 052 * Operators {@code &&} and {@code ||} in the body of target. 053 * </li> 054 * <li> 055 * {@code when} expression in case labels, also known as guards. 056 * </li> 057 * </ul> 058 * 059 * <p> 060 * By pure theory level 1-4 is considered easy to test, 5-7 OK, 8-10 consider 061 * re-factoring to ease testing, and 11+ re-factor now as testing will be painful. 062 * </p> 063 * 064 * <p> 065 * When it comes to code quality measurement by this metric level 10 is very 066 * good level as a ultimate target (that is hard to archive). Do not be ashamed 067 * to have complexity level 15 or even higher, but keep it below 20 to catch 068 * really bad-designed code automatically. 069 * </p> 070 * 071 * <p> 072 * Please use Suppression to avoid violations on cases that could not be split 073 * in few methods without damaging readability of code or encapsulation. 074 * </p> 075 * 076 * @since 3.2 077 */ 078@FileStatefulCheck 079public class CyclomaticComplexityCheck 080 extends AbstractCheck { 081 082 /** 083 * A key is pointing to the warning message text in "messages.properties" 084 * file. 085 */ 086 public static final String MSG_KEY = "cyclomaticComplexity"; 087 088 /** The initial current value. */ 089 private static final BigInteger INITIAL_VALUE = BigInteger.ONE; 090 091 /** Default allowed complexity. */ 092 private static final int DEFAULT_COMPLEXITY_VALUE = 10; 093 094 /** Stack of values - all but the current value. */ 095 private final Deque<BigInteger> valueStack = new ArrayDeque<>(); 096 097 /** Control whether to treat the whole switch block as a single decision point. */ 098 private boolean switchBlockAsSingleDecisionPoint; 099 100 /** The current value. */ 101 private BigInteger currentValue = INITIAL_VALUE; 102 103 /** Specify the maximum threshold allowed. */ 104 private int max = DEFAULT_COMPLEXITY_VALUE; 105 106 /** 107 * Setter to control whether to treat the whole switch block as a single decision point. 108 * 109 * @param switchBlockAsSingleDecisionPoint whether to treat the whole switch 110 * block as a single decision point. 111 * @since 6.11 112 */ 113 public void setSwitchBlockAsSingleDecisionPoint(boolean switchBlockAsSingleDecisionPoint) { 114 this.switchBlockAsSingleDecisionPoint = switchBlockAsSingleDecisionPoint; 115 } 116 117 /** 118 * Setter to specify the maximum threshold allowed. 119 * 120 * @param max the maximum threshold 121 * @since 3.2 122 */ 123 public final void setMax(int max) { 124 this.max = max; 125 } 126 127 @Override 128 public int[] getDefaultTokens() { 129 return new int[] { 130 TokenTypes.CTOR_DEF, 131 TokenTypes.METHOD_DEF, 132 TokenTypes.INSTANCE_INIT, 133 TokenTypes.STATIC_INIT, 134 TokenTypes.LITERAL_WHILE, 135 TokenTypes.LITERAL_DO, 136 TokenTypes.LITERAL_FOR, 137 TokenTypes.LITERAL_IF, 138 TokenTypes.LITERAL_SWITCH, 139 TokenTypes.LITERAL_CASE, 140 TokenTypes.LITERAL_CATCH, 141 TokenTypes.QUESTION, 142 TokenTypes.LAND, 143 TokenTypes.LOR, 144 TokenTypes.COMPACT_CTOR_DEF, 145 TokenTypes.LITERAL_WHEN, 146 }; 147 } 148 149 @Override 150 public int[] getAcceptableTokens() { 151 return new int[] { 152 TokenTypes.CTOR_DEF, 153 TokenTypes.METHOD_DEF, 154 TokenTypes.INSTANCE_INIT, 155 TokenTypes.STATIC_INIT, 156 TokenTypes.LITERAL_WHILE, 157 TokenTypes.LITERAL_DO, 158 TokenTypes.LITERAL_FOR, 159 TokenTypes.LITERAL_IF, 160 TokenTypes.LITERAL_SWITCH, 161 TokenTypes.LITERAL_CASE, 162 TokenTypes.LITERAL_CATCH, 163 TokenTypes.QUESTION, 164 TokenTypes.LAND, 165 TokenTypes.LOR, 166 TokenTypes.COMPACT_CTOR_DEF, 167 TokenTypes.LITERAL_WHEN, 168 }; 169 } 170 171 @Override 172 public final int[] getRequiredTokens() { 173 return new int[] { 174 TokenTypes.CTOR_DEF, 175 TokenTypes.METHOD_DEF, 176 TokenTypes.INSTANCE_INIT, 177 TokenTypes.STATIC_INIT, 178 TokenTypes.COMPACT_CTOR_DEF, 179 }; 180 } 181 182 @Override 183 public void visitToken(DetailAST ast) { 184 switch (ast.getType()) { 185 case TokenTypes.CTOR_DEF, 186 TokenTypes.METHOD_DEF, 187 TokenTypes.INSTANCE_INIT, 188 TokenTypes.STATIC_INIT, 189 TokenTypes.COMPACT_CTOR_DEF -> visitMethodDef(); 190 191 default -> visitTokenHook(ast); 192 } 193 } 194 195 @Override 196 public void leaveToken(DetailAST ast) { 197 switch (ast.getType()) { 198 case TokenTypes.CTOR_DEF, 199 TokenTypes.METHOD_DEF, 200 TokenTypes.INSTANCE_INIT, 201 TokenTypes.STATIC_INIT, 202 TokenTypes.COMPACT_CTOR_DEF -> leaveMethodDef(ast); 203 204 default -> { 205 // Do nothing 206 } 207 } 208 } 209 210 /** 211 * Hook called when visiting a token. Will not be called the method 212 * definition tokens. 213 * 214 * @param ast the token being visited 215 */ 216 private void visitTokenHook(DetailAST ast) { 217 if (switchBlockAsSingleDecisionPoint) { 218 if (!ScopeUtil.isInBlockOf(ast, TokenTypes.LITERAL_SWITCH)) { 219 incrementCurrentValue(BigInteger.ONE); 220 } 221 } 222 else if (ast.getType() != TokenTypes.LITERAL_SWITCH) { 223 incrementCurrentValue(BigInteger.ONE); 224 } 225 } 226 227 /** 228 * Process the end of a method definition. 229 * 230 * @param ast the token representing the method definition 231 */ 232 private void leaveMethodDef(DetailAST ast) { 233 final BigInteger bigIntegerMax = BigInteger.valueOf(max); 234 if (currentValue.compareTo(bigIntegerMax) > 0) { 235 log(ast, MSG_KEY, currentValue, bigIntegerMax); 236 } 237 popValue(); 238 } 239 240 /** 241 * Increments the current value by a specified amount. 242 * 243 * @param amount the amount to increment by 244 */ 245 private void incrementCurrentValue(BigInteger amount) { 246 currentValue = currentValue.add(amount); 247 } 248 249 /** Push the current value on the stack. */ 250 private void pushValue() { 251 valueStack.push(currentValue); 252 currentValue = INITIAL_VALUE; 253 } 254 255 /** 256 * Pops a value off the stack and makes it the current value. 257 */ 258 private void popValue() { 259 currentValue = valueStack.pop(); 260 } 261 262 /** Process the start of the method definition. */ 263 private void visitMethodDef() { 264 pushValue(); 265 } 266 267}