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