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 * <p> 33 * Restricts the number of boolean operators ({@code &&}, {@code ||}, 34 * {@code &}, {@code |} and {@code ^}) in an expression. 35 * </p> 36 * <p> 37 * Rationale: Too many conditions leads to code that is difficult to read 38 * and hence debug and maintain. 39 * </p> 40 * <p> 41 * Note that the operators {@code &} and {@code |} are not only integer bitwise 42 * operators, they are also the 43 * <a href="https://docs.oracle.com/javase/specs/jls/se11/html/jls-15.html#jls-15.22.2"> 44 * non-shortcut versions</a> of the boolean operators {@code &&} and {@code ||}. 45 * </p> 46 * <p> 47 * Note that {@code &}, {@code |} and {@code ^} are not checked if they are part 48 * of constructor or method call because they can be applied to non-boolean 49 * variables and Checkstyle does not know types of methods from different classes. 50 * </p> 51 * <ul> 52 * <li> 53 * Property {@code max} - Specify the maximum number of boolean operations 54 * allowed in one expression. 55 * Type is {@code int}. 56 * Default value is {@code 3}. 57 * </li> 58 * <li> 59 * Property {@code tokens} - tokens to check 60 * Type is {@code java.lang.String[]}. 61 * Validation type is {@code tokenSet}. 62 * Default value is: 63 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LAND"> 64 * LAND</a>, 65 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#BAND"> 66 * BAND</a>, 67 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LOR"> 68 * LOR</a>, 69 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#BOR"> 70 * BOR</a>, 71 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#BXOR"> 72 * BXOR</a>. 73 * </li> 74 * </ul> 75 * <p> 76 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 77 * </p> 78 * <p> 79 * Violation Message Keys: 80 * </p> 81 * <ul> 82 * <li> 83 * {@code booleanExpressionComplexity} 84 * </li> 85 * </ul> 86 * 87 * @since 3.4 88 */ 89 @FileStatefulCheck 90 public final class BooleanExpressionComplexityCheck extends AbstractCheck { 91 92 /** 93 * A key is pointing to the warning message text in "messages.properties" 94 * file. 95 */ 96 public static final String MSG_KEY = "booleanExpressionComplexity"; 97 98 /** Default allowed complexity. */ 99 private static final int DEFAULT_MAX = 3; 100 101 /** Stack of contexts. */ 102 private final Deque<Context> contextStack = new ArrayDeque<>(); 103 /** Specify the maximum number of boolean operations allowed in one expression. */ 104 private int max; 105 /** Current context. */ 106 private Context context = new Context(false); 107 108 /** Creates new instance of the check. */ 109 public BooleanExpressionComplexityCheck() { 110 max = DEFAULT_MAX; 111 } 112 113 @Override 114 public int[] getDefaultTokens() { 115 return new int[] { 116 TokenTypes.CTOR_DEF, 117 TokenTypes.METHOD_DEF, 118 TokenTypes.EXPR, 119 TokenTypes.LAND, 120 TokenTypes.BAND, 121 TokenTypes.LOR, 122 TokenTypes.BOR, 123 TokenTypes.BXOR, 124 TokenTypes.COMPACT_CTOR_DEF, 125 }; 126 } 127 128 @Override 129 public int[] getRequiredTokens() { 130 return new int[] { 131 TokenTypes.CTOR_DEF, 132 TokenTypes.METHOD_DEF, 133 TokenTypes.EXPR, 134 TokenTypes.COMPACT_CTOR_DEF, 135 }; 136 } 137 138 @Override 139 public int[] getAcceptableTokens() { 140 return new int[] { 141 TokenTypes.CTOR_DEF, 142 TokenTypes.METHOD_DEF, 143 TokenTypes.EXPR, 144 TokenTypes.LAND, 145 TokenTypes.BAND, 146 TokenTypes.LOR, 147 TokenTypes.BOR, 148 TokenTypes.BXOR, 149 TokenTypes.COMPACT_CTOR_DEF, 150 }; 151 } 152 153 /** 154 * Setter to specify the maximum number of boolean operations allowed in one expression. 155 * 156 * @param max new maximum allowed complexity. 157 * @since 3.4 158 */ 159 public void setMax(int max) { 160 this.max = max; 161 } 162 163 @Override 164 public void visitToken(DetailAST ast) { 165 switch (ast.getType()) { 166 case TokenTypes.CTOR_DEF: 167 case TokenTypes.METHOD_DEF: 168 case TokenTypes.COMPACT_CTOR_DEF: 169 visitMethodDef(ast); 170 break; 171 case TokenTypes.EXPR: 172 visitExpr(); 173 break; 174 case TokenTypes.BOR: 175 if (!isPipeOperator(ast) && !isPassedInParameter(ast)) { 176 context.visitBooleanOperator(); 177 } 178 break; 179 case TokenTypes.BAND: 180 case TokenTypes.BXOR: 181 if (!isPassedInParameter(ast)) { 182 context.visitBooleanOperator(); 183 } 184 break; 185 case TokenTypes.LAND: 186 case TokenTypes.LOR: 187 context.visitBooleanOperator(); 188 break; 189 default: 190 throw new IllegalArgumentException("Unknown type: " + ast); 191 } 192 } 193 194 /** 195 * Checks if logical operator is part of constructor or method call. 196 * 197 * @param logicalOperator logical operator 198 * @return true if logical operator is part of constructor or method call 199 */ 200 private static boolean isPassedInParameter(DetailAST logicalOperator) { 201 return logicalOperator.getParent().getParent().getType() == TokenTypes.ELIST; 202 } 203 204 /** 205 * Checks if {@link TokenTypes#BOR binary OR} is applied to exceptions 206 * in 207 * <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-14.html#jls-14.20"> 208 * multi-catch</a> (pipe-syntax). 209 * 210 * @param binaryOr {@link TokenTypes#BOR binary or} 211 * @return true if binary or is applied to exceptions in multi-catch. 212 */ 213 private static boolean isPipeOperator(DetailAST binaryOr) { 214 return binaryOr.getParent().getType() == TokenTypes.TYPE; 215 } 216 217 @Override 218 public void leaveToken(DetailAST ast) { 219 switch (ast.getType()) { 220 case TokenTypes.CTOR_DEF: 221 case TokenTypes.METHOD_DEF: 222 case TokenTypes.COMPACT_CTOR_DEF: 223 leaveMethodDef(); 224 break; 225 case TokenTypes.EXPR: 226 leaveExpr(ast); 227 break; 228 default: 229 // Do nothing 230 } 231 } 232 233 /** 234 * Creates new context for a given method. 235 * 236 * @param ast a method we start to check. 237 */ 238 private void visitMethodDef(DetailAST ast) { 239 contextStack.push(context); 240 final boolean check = !CheckUtil.isEqualsMethod(ast); 241 context = new Context(check); 242 } 243 244 /** Removes old context. */ 245 private void leaveMethodDef() { 246 context = contextStack.pop(); 247 } 248 249 /** Creates and pushes new context. */ 250 private void visitExpr() { 251 contextStack.push(context); 252 context = new Context(context.isChecking()); 253 } 254 255 /** 256 * Restores previous context. 257 * 258 * @param ast expression we leave. 259 */ 260 private void leaveExpr(DetailAST ast) { 261 context.checkCount(ast); 262 context = contextStack.pop(); 263 } 264 265 /** 266 * Represents context (method/expression) in which we check complexity. 267 * 268 */ 269 private final class Context { 270 271 /** 272 * Should we perform check in current context or not. 273 * Usually false if we are inside equals() method. 274 */ 275 private final boolean checking; 276 /** Count of boolean operators. */ 277 private int count; 278 279 /** 280 * Creates new instance. 281 * 282 * @param checking should we check in current context or not. 283 */ 284 private Context(boolean checking) { 285 this.checking = checking; 286 } 287 288 /** 289 * Getter for checking property. 290 * 291 * @return should we check in current context or not. 292 */ 293 public boolean isChecking() { 294 return checking; 295 } 296 297 /** Increases operator counter. */ 298 public void visitBooleanOperator() { 299 ++count; 300 } 301 302 /** 303 * Checks if we violate maximum allowed complexity. 304 * 305 * @param ast a node we check now. 306 */ 307 public void checkCount(DetailAST ast) { 308 if (checking && count > max) { 309 final DetailAST parentAST = ast.getParent(); 310 311 log(parentAST, MSG_KEY, count, max); 312 } 313 } 314 315 } 316 317 }