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