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