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.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; 029import com.puppycrawl.tools.checkstyle.utils.CheckUtil; 030 031/** 032 * <div> 033 * Restricts the number of boolean operators ({@code &&}, {@code ||}, 034 * {@code &}, {@code |} and {@code ^}) in an expression. 035 * </div> 036 * 037 * <p> 038 * Rationale: Too many conditions leads to code that is difficult to read 039 * and hence debug and maintain. 040 * </p> 041 * 042 * <p> 043 * Note that the operators {@code &} and {@code |} are not only integer bitwise 044 * operators, they are also the 045 * <a href="https://docs.oracle.com/javase/specs/jls/se11/html/jls-15.html#jls-15.22.2"> 046 * non-shortcut versions</a> of the boolean operators {@code &&} and {@code ||}. 047 * </p> 048 * 049 * <p> 050 * Note that {@code &}, {@code |} and {@code ^} are not checked if they are part 051 * of constructor or method call because they can be applied to non-boolean 052 * variables and Checkstyle does not know types of methods from different classes. 053 * </p> 054 * <ul> 055 * <li> 056 * Property {@code max} - Specify the maximum number of boolean operations 057 * allowed in one expression. 058 * Type is {@code int}. 059 * Default value is {@code 3}. 060 * </li> 061 * <li> 062 * Property {@code tokens} - tokens to check 063 * Type is {@code java.lang.String[]}. 064 * Validation type is {@code tokenSet}. 065 * Default value is: 066 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LAND"> 067 * LAND</a>, 068 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#BAND"> 069 * BAND</a>, 070 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LOR"> 071 * LOR</a>, 072 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#BOR"> 073 * BOR</a>, 074 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#BXOR"> 075 * BXOR</a>. 076 * </li> 077 * </ul> 078 * 079 * <p> 080 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 081 * </p> 082 * 083 * <p> 084 * Violation Message Keys: 085 * </p> 086 * <ul> 087 * <li> 088 * {@code booleanExpressionComplexity} 089 * </li> 090 * </ul> 091 * 092 * @since 3.4 093 */ 094@FileStatefulCheck 095public final class BooleanExpressionComplexityCheck extends AbstractCheck { 096 097 /** 098 * A key is pointing to the warning message text in "messages.properties" 099 * file. 100 */ 101 public static final String MSG_KEY = "booleanExpressionComplexity"; 102 103 /** Default allowed complexity. */ 104 private static final int DEFAULT_MAX = 3; 105 106 /** Stack of contexts. */ 107 private final Deque<Context> contextStack = new ArrayDeque<>(); 108 /** Specify the maximum number of boolean operations allowed in one expression. */ 109 private int max; 110 /** Current context. */ 111 private Context context = new Context(false); 112 113 /** Creates new instance of the check. */ 114 public BooleanExpressionComplexityCheck() { 115 max = DEFAULT_MAX; 116 } 117 118 @Override 119 public int[] getDefaultTokens() { 120 return new int[] { 121 TokenTypes.CTOR_DEF, 122 TokenTypes.METHOD_DEF, 123 TokenTypes.EXPR, 124 TokenTypes.LAND, 125 TokenTypes.BAND, 126 TokenTypes.LOR, 127 TokenTypes.BOR, 128 TokenTypes.BXOR, 129 TokenTypes.COMPACT_CTOR_DEF, 130 }; 131 } 132 133 @Override 134 public int[] getRequiredTokens() { 135 return new int[] { 136 TokenTypes.CTOR_DEF, 137 TokenTypes.METHOD_DEF, 138 TokenTypes.EXPR, 139 TokenTypes.COMPACT_CTOR_DEF, 140 }; 141 } 142 143 @Override 144 public int[] getAcceptableTokens() { 145 return new int[] { 146 TokenTypes.CTOR_DEF, 147 TokenTypes.METHOD_DEF, 148 TokenTypes.EXPR, 149 TokenTypes.LAND, 150 TokenTypes.BAND, 151 TokenTypes.LOR, 152 TokenTypes.BOR, 153 TokenTypes.BXOR, 154 TokenTypes.COMPACT_CTOR_DEF, 155 }; 156 } 157 158 /** 159 * Setter to specify the maximum number of boolean operations allowed in one expression. 160 * 161 * @param max new maximum allowed complexity. 162 * @since 3.4 163 */ 164 public void setMax(int max) { 165 this.max = max; 166 } 167 168 @Override 169 public void visitToken(DetailAST ast) { 170 switch (ast.getType()) { 171 case TokenTypes.CTOR_DEF, 172 TokenTypes.METHOD_DEF, 173 TokenTypes.COMPACT_CTOR_DEF -> visitMethodDef(ast); 174 175 case TokenTypes.EXPR -> visitExpr(); 176 177 case TokenTypes.BOR -> { 178 if (!isPipeOperator(ast) && !isPassedInParameter(ast)) { 179 context.visitBooleanOperator(); 180 } 181 } 182 183 case TokenTypes.BAND, 184 TokenTypes.BXOR -> { 185 if (!isPassedInParameter(ast)) { 186 context.visitBooleanOperator(); 187 } 188 } 189 190 case TokenTypes.LAND, 191 TokenTypes.LOR -> context.visitBooleanOperator(); 192 193 default -> throw new IllegalArgumentException("Unknown type: " + ast); 194 } 195 } 196 197 /** 198 * Checks if logical operator is part of constructor or method call. 199 * 200 * @param logicalOperator logical operator 201 * @return true if logical operator is part of constructor or method call 202 */ 203 private static boolean isPassedInParameter(DetailAST logicalOperator) { 204 return logicalOperator.getParent().getParent().getType() == TokenTypes.ELIST; 205 } 206 207 /** 208 * Checks if {@link TokenTypes#BOR binary OR} is applied to exceptions 209 * in 210 * <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-14.html#jls-14.20"> 211 * multi-catch</a> (pipe-syntax). 212 * 213 * @param binaryOr {@link TokenTypes#BOR binary or} 214 * @return true if binary or is applied to exceptions in multi-catch. 215 */ 216 private static boolean isPipeOperator(DetailAST binaryOr) { 217 return binaryOr.getParent().getType() == TokenTypes.TYPE; 218 } 219 220 @Override 221 public void leaveToken(DetailAST ast) { 222 switch (ast.getType()) { 223 case TokenTypes.CTOR_DEF, 224 TokenTypes.METHOD_DEF, 225 TokenTypes.COMPACT_CTOR_DEF -> leaveMethodDef(); 226 227 case TokenTypes.EXPR -> leaveExpr(ast); 228 229 default -> { 230 // Do nothing 231 } 232 } 233 } 234 235 /** 236 * Creates new context for a given method. 237 * 238 * @param ast a method we start to check. 239 */ 240 private void visitMethodDef(DetailAST ast) { 241 contextStack.push(context); 242 final boolean check = !CheckUtil.isEqualsMethod(ast); 243 context = new Context(check); 244 } 245 246 /** Removes old context. */ 247 private void leaveMethodDef() { 248 context = contextStack.pop(); 249 } 250 251 /** Creates and pushes new context. */ 252 private void visitExpr() { 253 contextStack.push(context); 254 context = new Context(context.isChecking()); 255 } 256 257 /** 258 * Restores previous context. 259 * 260 * @param ast expression we leave. 261 */ 262 private void leaveExpr(DetailAST ast) { 263 context.checkCount(ast); 264 context = contextStack.pop(); 265 } 266 267 /** 268 * Represents context (method/expression) in which we check complexity. 269 * 270 */ 271 private final class Context { 272 273 /** 274 * Should we perform check in current context or not. 275 * Usually false if we are inside equals() method. 276 */ 277 private final boolean checking; 278 /** Count of boolean operators. */ 279 private int count; 280 281 /** 282 * Creates new instance. 283 * 284 * @param checking should we check in current context or not. 285 */ 286 private Context(boolean checking) { 287 this.checking = checking; 288 } 289 290 /** 291 * Getter for checking property. 292 * 293 * @return should we check in current context or not. 294 */ 295 public boolean isChecking() { 296 return checking; 297 } 298 299 /** Increases operator counter. */ 300 public void visitBooleanOperator() { 301 ++count; 302 } 303 304 /** 305 * Checks if we violate maximum allowed complexity. 306 * 307 * @param ast a node we check now. 308 */ 309 public void checkCount(DetailAST ast) { 310 if (checking && count > max) { 311 final DetailAST parentAST = ast.getParent(); 312 313 log(parentAST, MSG_KEY, count, max); 314 } 315 } 316 317 } 318 319}