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 * 055 * @since 3.4 056 */ 057@FileStatefulCheck 058public final class BooleanExpressionComplexityCheck extends AbstractCheck { 059 060 /** 061 * A key is pointing to the warning message text in "messages.properties" 062 * file. 063 */ 064 public static final String MSG_KEY = "booleanExpressionComplexity"; 065 066 /** Default allowed complexity. */ 067 private static final int DEFAULT_MAX = 3; 068 069 /** Stack of contexts. */ 070 private final Deque<Context> contextStack = new ArrayDeque<>(); 071 /** Specify the maximum number of boolean operations allowed in one expression. */ 072 private int max; 073 /** Current context. */ 074 private Context context = new Context(false); 075 076 /** Creates new instance of the check. */ 077 public BooleanExpressionComplexityCheck() { 078 max = DEFAULT_MAX; 079 } 080 081 @Override 082 public int[] getDefaultTokens() { 083 return new int[] { 084 TokenTypes.CTOR_DEF, 085 TokenTypes.METHOD_DEF, 086 TokenTypes.EXPR, 087 TokenTypes.LAND, 088 TokenTypes.BAND, 089 TokenTypes.LOR, 090 TokenTypes.BOR, 091 TokenTypes.BXOR, 092 TokenTypes.COMPACT_CTOR_DEF, 093 }; 094 } 095 096 @Override 097 public int[] getRequiredTokens() { 098 return new int[] { 099 TokenTypes.CTOR_DEF, 100 TokenTypes.METHOD_DEF, 101 TokenTypes.EXPR, 102 TokenTypes.COMPACT_CTOR_DEF, 103 }; 104 } 105 106 @Override 107 public int[] getAcceptableTokens() { 108 return new int[] { 109 TokenTypes.CTOR_DEF, 110 TokenTypes.METHOD_DEF, 111 TokenTypes.EXPR, 112 TokenTypes.LAND, 113 TokenTypes.BAND, 114 TokenTypes.LOR, 115 TokenTypes.BOR, 116 TokenTypes.BXOR, 117 TokenTypes.COMPACT_CTOR_DEF, 118 }; 119 } 120 121 /** 122 * Setter to specify the maximum number of boolean operations allowed in one expression. 123 * 124 * @param max new maximum allowed complexity. 125 * @since 3.4 126 */ 127 public void setMax(int max) { 128 this.max = max; 129 } 130 131 @Override 132 public void visitToken(DetailAST ast) { 133 switch (ast.getType()) { 134 case TokenTypes.CTOR_DEF, 135 TokenTypes.METHOD_DEF, 136 TokenTypes.COMPACT_CTOR_DEF -> visitMethodDef(ast); 137 138 case TokenTypes.EXPR -> visitExpr(); 139 140 case TokenTypes.BOR -> { 141 if (!isPipeOperator(ast) && !isPassedInParameter(ast)) { 142 context.visitBooleanOperator(); 143 } 144 } 145 146 case TokenTypes.BAND, 147 TokenTypes.BXOR -> { 148 if (!isPassedInParameter(ast)) { 149 context.visitBooleanOperator(); 150 } 151 } 152 153 case TokenTypes.LAND, 154 TokenTypes.LOR -> context.visitBooleanOperator(); 155 156 default -> throw new IllegalArgumentException("Unknown type: " + ast); 157 } 158 } 159 160 /** 161 * Checks if logical operator is part of constructor or method call. 162 * 163 * @param logicalOperator logical operator 164 * @return true if logical operator is part of constructor or method call 165 */ 166 private static boolean isPassedInParameter(DetailAST logicalOperator) { 167 return logicalOperator.getParent().getParent().getType() == TokenTypes.ELIST; 168 } 169 170 /** 171 * Checks if {@link TokenTypes#BOR binary OR} is applied to exceptions 172 * in 173 * <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-14.html#jls-14.20"> 174 * multi-catch</a> (pipe-syntax). 175 * 176 * @param binaryOr {@link TokenTypes#BOR binary or} 177 * @return true if binary or is applied to exceptions in multi-catch. 178 */ 179 private static boolean isPipeOperator(DetailAST binaryOr) { 180 return binaryOr.getParent().getType() == TokenTypes.TYPE; 181 } 182 183 @Override 184 public void leaveToken(DetailAST ast) { 185 switch (ast.getType()) { 186 case TokenTypes.CTOR_DEF, 187 TokenTypes.METHOD_DEF, 188 TokenTypes.COMPACT_CTOR_DEF -> leaveMethodDef(); 189 190 case TokenTypes.EXPR -> leaveExpr(ast); 191 192 default -> { 193 // Do nothing 194 } 195 } 196 } 197 198 /** 199 * Creates new context for a given method. 200 * 201 * @param ast a method we start to check. 202 */ 203 private void visitMethodDef(DetailAST ast) { 204 contextStack.push(context); 205 final boolean check = !CheckUtil.isEqualsMethod(ast); 206 context = new Context(check); 207 } 208 209 /** Removes old context. */ 210 private void leaveMethodDef() { 211 context = contextStack.pop(); 212 } 213 214 /** Creates and pushes new context. */ 215 private void visitExpr() { 216 contextStack.push(context); 217 context = new Context(context.isChecking()); 218 } 219 220 /** 221 * Restores previous context. 222 * 223 * @param ast expression we leave. 224 */ 225 private void leaveExpr(DetailAST ast) { 226 context.checkCount(ast); 227 context = contextStack.pop(); 228 } 229 230 /** 231 * Represents context (method/expression) in which we check complexity. 232 * 233 */ 234 private final class Context { 235 236 /** 237 * Should we perform check in current context or not. 238 * Usually false if we are inside equals() method. 239 */ 240 private final boolean checking; 241 /** Count of boolean operators. */ 242 private int count; 243 244 /** 245 * Creates new instance. 246 * 247 * @param checking should we check in current context or not. 248 */ 249 private Context(boolean checking) { 250 this.checking = checking; 251 } 252 253 /** 254 * Getter for checking property. 255 * 256 * @return should we check in current context or not. 257 */ 258 public boolean isChecking() { 259 return checking; 260 } 261 262 /** Increases operator counter. */ 263 public void visitBooleanOperator() { 264 ++count; 265 } 266 267 /** 268 * Checks if we violate maximum allowed complexity. 269 * 270 * @param ast a node we check now. 271 */ 272 public void checkCount(DetailAST ast) { 273 if (checking && count > max) { 274 final DetailAST parentAST = ast.getParent(); 275 276 log(parentAST, MSG_KEY, count, max); 277 } 278 } 279 280 } 281 282}