001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2019 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 * Restrict the number of {@code &&}, {@code ||}, {@code &}, {@code |} and 034 * {@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> 078 * To configure the check with 7 allowed operation in boolean expression: 079 * </p> 080 * <pre> 081 * <module name="BooleanExpressionComplexity"> 082 * <property name="max" value="7"/> 083 * </module> 084 * </pre> 085 * <p> 086 * To configure the check to ignore {@code &} and {@code |}: 087 * </p> 088 * <pre> 089 * <module name="BooleanExpressionComplexity"> 090 * <property name="tokens" value="BXOR,LAND,LOR"/> 091 * </module> 092 * </pre> 093 * 094 * @since 3.4 095 */ 096@FileStatefulCheck 097public final class BooleanExpressionComplexityCheck extends AbstractCheck { 098 099 /** 100 * A key is pointing to the warning message text in "messages.properties" 101 * file. 102 */ 103 public static final String MSG_KEY = "booleanExpressionComplexity"; 104 105 /** Default allowed complexity. */ 106 private static final int DEFAULT_MAX = 3; 107 108 /** Stack of contexts. */ 109 private final Deque<Context> contextStack = new ArrayDeque<>(); 110 /** Specify the maximum number of boolean operations allowed in one expression. */ 111 private int max; 112 /** Current context. */ 113 private Context context = new Context(false); 114 115 /** Creates new instance of the check. */ 116 public BooleanExpressionComplexityCheck() { 117 max = DEFAULT_MAX; 118 } 119 120 @Override 121 public int[] getDefaultTokens() { 122 return new int[] { 123 TokenTypes.CTOR_DEF, 124 TokenTypes.METHOD_DEF, 125 TokenTypes.EXPR, 126 TokenTypes.LAND, 127 TokenTypes.BAND, 128 TokenTypes.LOR, 129 TokenTypes.BOR, 130 TokenTypes.BXOR, 131 }; 132 } 133 134 @Override 135 public int[] getRequiredTokens() { 136 return new int[] { 137 TokenTypes.CTOR_DEF, 138 TokenTypes.METHOD_DEF, 139 TokenTypes.EXPR, 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 }; 155 } 156 157 /** 158 * Setter to specify the maximum number of boolean operations allowed in one expression. 159 * 160 * @param max new maximum allowed complexity. 161 */ 162 public void setMax(int max) { 163 this.max = max; 164 } 165 166 @Override 167 public void visitToken(DetailAST ast) { 168 switch (ast.getType()) { 169 case TokenTypes.CTOR_DEF: 170 case TokenTypes.METHOD_DEF: 171 visitMethodDef(ast); 172 break; 173 case TokenTypes.EXPR: 174 visitExpr(); 175 break; 176 case TokenTypes.BOR: 177 if (!isPipeOperator(ast) && !isPassedInParameter(ast)) { 178 context.visitBooleanOperator(); 179 } 180 break; 181 case TokenTypes.BAND: 182 case TokenTypes.BXOR: 183 if (!isPassedInParameter(ast)) { 184 context.visitBooleanOperator(); 185 } 186 break; 187 case TokenTypes.LAND: 188 case TokenTypes.LOR: 189 context.visitBooleanOperator(); 190 break; 191 default: 192 throw new IllegalArgumentException("Unknown type: " + ast); 193 } 194 } 195 196 /** 197 * Checks if logical operator is part of constructor or method call. 198 * @param logicalOperator logical operator 199 * @return true if logical operator is part of constructor or method call 200 */ 201 private static boolean isPassedInParameter(DetailAST logicalOperator) { 202 return logicalOperator.getParent().getParent().getType() == TokenTypes.ELIST; 203 } 204 205 /** 206 * Checks if {@link TokenTypes#BOR binary OR} is applied to exceptions 207 * in 208 * <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-14.html#jls-14.20"> 209 * multi-catch</a> (pipe-syntax). 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 leaveMethodDef(); 223 break; 224 case TokenTypes.EXPR: 225 leaveExpr(ast); 226 break; 227 default: 228 // Do nothing 229 } 230 } 231 232 /** 233 * Creates new context for a given method. 234 * @param ast a method we start to check. 235 */ 236 private void visitMethodDef(DetailAST ast) { 237 contextStack.push(context); 238 final boolean check = !CheckUtil.isEqualsMethod(ast); 239 context = new Context(check); 240 } 241 242 /** Removes old context. */ 243 private void leaveMethodDef() { 244 context = contextStack.pop(); 245 } 246 247 /** Creates and pushes new context. */ 248 private void visitExpr() { 249 contextStack.push(context); 250 context = new Context(context.isChecking()); 251 } 252 253 /** 254 * Restores previous context. 255 * @param ast expression we leave. 256 */ 257 private void leaveExpr(DetailAST ast) { 258 context.checkCount(ast); 259 context = contextStack.pop(); 260 } 261 262 /** 263 * Represents context (method/expression) in which we check complexity. 264 * 265 */ 266 private class Context { 267 268 /** 269 * Should we perform check in current context or not. 270 * Usually false if we are inside equals() method. 271 */ 272 private final boolean checking; 273 /** Count of boolean operators. */ 274 private int count; 275 276 /** 277 * Creates new instance. 278 * @param checking should we check in current context or not. 279 */ 280 /* package */ Context(boolean checking) { 281 this.checking = checking; 282 count = 0; 283 } 284 285 /** 286 * Getter for checking property. 287 * @return should we check in current context or not. 288 */ 289 public boolean isChecking() { 290 return checking; 291 } 292 293 /** Increases operator counter. */ 294 public void visitBooleanOperator() { 295 ++count; 296 } 297 298 /** 299 * Checks if we violates maximum allowed complexity. 300 * @param ast a node we check now. 301 */ 302 public void checkCount(DetailAST ast) { 303 if (checking && count > max) { 304 final DetailAST parentAST = ast.getParent(); 305 306 log(parentAST, MSG_KEY, count, max); 307 } 308 } 309 310 } 311 312}