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