1 /////////////////////////////////////////////////////////////////////////////////////////////// 2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules. 3 // Copyright (C) 2001-2024 the original author or authors. 4 // 5 // This library is free software; you can redistribute it and/or 6 // modify it under the terms of the GNU Lesser General Public 7 // License as published by the Free Software Foundation; either 8 // version 2.1 of the License, or (at your option) any later version. 9 // 10 // This library is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 // Lesser General Public License for more details. 14 // 15 // You should have received a copy of the GNU Lesser General Public 16 // License along with this library; if not, write to the Free Software 17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 /////////////////////////////////////////////////////////////////////////////////////////////// 19 20 package com.puppycrawl.tools.checkstyle.checks.metrics; 21 22 import java.util.ArrayDeque; 23 import java.util.Deque; 24 25 import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 26 import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 27 import com.puppycrawl.tools.checkstyle.api.DetailAST; 28 import com.puppycrawl.tools.checkstyle.api.TokenTypes; 29 import com.puppycrawl.tools.checkstyle.utils.CheckUtil; 30 31 /** 32 * <div> 33 * Restricts the number of boolean operators ({@code &&}, {@code ||}, 34 * {@code &}, {@code |} and {@code ^}) in an expression. 35 * </div> 36 * 37 * <p> 38 * Rationale: Too many conditions leads to code that is difficult to read 39 * and hence debug and maintain. 40 * </p> 41 * 42 * <p> 43 * Note that the operators {@code &} and {@code |} are not only integer bitwise 44 * operators, they are also the 45 * <a href="https://docs.oracle.com/javase/specs/jls/se11/html/jls-15.html#jls-15.22.2"> 46 * non-shortcut versions</a> of the boolean operators {@code &&} and {@code ||}. 47 * </p> 48 * 49 * <p> 50 * Note that {@code &}, {@code |} and {@code ^} are not checked if they are part 51 * of constructor or method call because they can be applied to non-boolean 52 * variables and Checkstyle does not know types of methods from different classes. 53 * </p> 54 * <ul> 55 * <li> 56 * Property {@code max} - Specify the maximum number of boolean operations 57 * allowed in one expression. 58 * Type is {@code int}. 59 * Default value is {@code 3}. 60 * </li> 61 * <li> 62 * Property {@code tokens} - tokens to check 63 * Type is {@code java.lang.String[]}. 64 * Validation type is {@code tokenSet}. 65 * Default value is: 66 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LAND"> 67 * LAND</a>, 68 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#BAND"> 69 * BAND</a>, 70 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LOR"> 71 * LOR</a>, 72 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#BOR"> 73 * BOR</a>, 74 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#BXOR"> 75 * BXOR</a>. 76 * </li> 77 * </ul> 78 * 79 * <p> 80 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 81 * </p> 82 * 83 * <p> 84 * Violation Message Keys: 85 * </p> 86 * <ul> 87 * <li> 88 * {@code booleanExpressionComplexity} 89 * </li> 90 * </ul> 91 * 92 * @since 3.4 93 */ 94 @FileStatefulCheck 95 public final class BooleanExpressionComplexityCheck extends AbstractCheck { 96 97 /** 98 * A key is pointing to the warning message text in "messages.properties" 99 * 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 case TokenTypes.METHOD_DEF: 173 case TokenTypes.COMPACT_CTOR_DEF: 174 visitMethodDef(ast); 175 break; 176 case TokenTypes.EXPR: 177 visitExpr(); 178 break; 179 case TokenTypes.BOR: 180 if (!isPipeOperator(ast) && !isPassedInParameter(ast)) { 181 context.visitBooleanOperator(); 182 } 183 break; 184 case TokenTypes.BAND: 185 case TokenTypes.BXOR: 186 if (!isPassedInParameter(ast)) { 187 context.visitBooleanOperator(); 188 } 189 break; 190 case TokenTypes.LAND: 191 case TokenTypes.LOR: 192 context.visitBooleanOperator(); 193 break; 194 default: 195 throw new IllegalArgumentException("Unknown type: " + ast); 196 } 197 } 198 199 /** 200 * Checks if logical operator is part of constructor or method call. 201 * 202 * @param logicalOperator logical operator 203 * @return true if logical operator is part of constructor or method call 204 */ 205 private static boolean isPassedInParameter(DetailAST logicalOperator) { 206 return logicalOperator.getParent().getParent().getType() == TokenTypes.ELIST; 207 } 208 209 /** 210 * Checks if {@link TokenTypes#BOR binary OR} is applied to exceptions 211 * in 212 * <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-14.html#jls-14.20"> 213 * multi-catch</a> (pipe-syntax). 214 * 215 * @param binaryOr {@link TokenTypes#BOR binary or} 216 * @return true if binary or is applied to exceptions in multi-catch. 217 */ 218 private static boolean isPipeOperator(DetailAST binaryOr) { 219 return binaryOr.getParent().getType() == TokenTypes.TYPE; 220 } 221 222 @Override 223 public void leaveToken(DetailAST ast) { 224 switch (ast.getType()) { 225 case TokenTypes.CTOR_DEF: 226 case TokenTypes.METHOD_DEF: 227 case TokenTypes.COMPACT_CTOR_DEF: 228 leaveMethodDef(); 229 break; 230 case TokenTypes.EXPR: 231 leaveExpr(ast); 232 break; 233 default: 234 // Do nothing 235 } 236 } 237 238 /** 239 * Creates new context for a given method. 240 * 241 * @param ast a method we start to check. 242 */ 243 private void visitMethodDef(DetailAST ast) { 244 contextStack.push(context); 245 final boolean check = !CheckUtil.isEqualsMethod(ast); 246 context = new Context(check); 247 } 248 249 /** Removes old context. */ 250 private void leaveMethodDef() { 251 context = contextStack.pop(); 252 } 253 254 /** Creates and pushes new context. */ 255 private void visitExpr() { 256 contextStack.push(context); 257 context = new Context(context.isChecking()); 258 } 259 260 /** 261 * Restores previous context. 262 * 263 * @param ast expression we leave. 264 */ 265 private void leaveExpr(DetailAST ast) { 266 context.checkCount(ast); 267 context = contextStack.pop(); 268 } 269 270 /** 271 * Represents context (method/expression) in which we check complexity. 272 * 273 */ 274 private final class Context { 275 276 /** 277 * Should we perform check in current context or not. 278 * Usually false if we are inside equals() method. 279 */ 280 private final boolean checking; 281 /** Count of boolean operators. */ 282 private int count; 283 284 /** 285 * Creates new instance. 286 * 287 * @param checking should we check in current context or not. 288 */ 289 private Context(boolean checking) { 290 this.checking = checking; 291 } 292 293 /** 294 * Getter for checking property. 295 * 296 * @return should we check in current context or not. 297 */ 298 public boolean isChecking() { 299 return checking; 300 } 301 302 /** Increases operator counter. */ 303 public void visitBooleanOperator() { 304 ++count; 305 } 306 307 /** 308 * Checks if we violate maximum allowed complexity. 309 * 310 * @param ast a node we check now. 311 */ 312 public void checkCount(DetailAST ast) { 313 if (checking && count > max) { 314 final DetailAST parentAST = ast.getParent(); 315 316 log(parentAST, MSG_KEY, count, max); 317 } 318 } 319 320 } 321 322 }