1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2025 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 *
55 * @since 3.4
56 */
57 @FileStatefulCheck
58 public final class BooleanExpressionComplexityCheck extends AbstractCheck {
59
60 /**
61 * A key is pointing to the warning message text in "messages.properties"
62 * file.
63 */
64 public static final String MSG_KEY = "booleanExpressionComplexity";
65
66 /** Default allowed complexity. */
67 private static final int DEFAULT_MAX = 3;
68
69 /** Stack of contexts. */
70 private final Deque<Context> contextStack = new ArrayDeque<>();
71 /** Specify the maximum number of boolean operations allowed in one expression. */
72 private int max;
73 /** Current context. */
74 private Context context = new Context(false);
75
76 /** Creates new instance of the check. */
77 public BooleanExpressionComplexityCheck() {
78 max = DEFAULT_MAX;
79 }
80
81 @Override
82 public int[] getDefaultTokens() {
83 return new int[] {
84 TokenTypes.CTOR_DEF,
85 TokenTypes.METHOD_DEF,
86 TokenTypes.EXPR,
87 TokenTypes.LAND,
88 TokenTypes.BAND,
89 TokenTypes.LOR,
90 TokenTypes.BOR,
91 TokenTypes.BXOR,
92 TokenTypes.COMPACT_CTOR_DEF,
93 };
94 }
95
96 @Override
97 public int[] getRequiredTokens() {
98 return new int[] {
99 TokenTypes.CTOR_DEF,
100 TokenTypes.METHOD_DEF,
101 TokenTypes.EXPR,
102 TokenTypes.COMPACT_CTOR_DEF,
103 };
104 }
105
106 @Override
107 public int[] getAcceptableTokens() {
108 return new int[] {
109 TokenTypes.CTOR_DEF,
110 TokenTypes.METHOD_DEF,
111 TokenTypes.EXPR,
112 TokenTypes.LAND,
113 TokenTypes.BAND,
114 TokenTypes.LOR,
115 TokenTypes.BOR,
116 TokenTypes.BXOR,
117 TokenTypes.COMPACT_CTOR_DEF,
118 };
119 }
120
121 /**
122 * Setter to specify the maximum number of boolean operations allowed in one expression.
123 *
124 * @param max new maximum allowed complexity.
125 * @since 3.4
126 */
127 public void setMax(int max) {
128 this.max = max;
129 }
130
131 @Override
132 public void visitToken(DetailAST ast) {
133 switch (ast.getType()) {
134 case TokenTypes.CTOR_DEF,
135 TokenTypes.METHOD_DEF,
136 TokenTypes.COMPACT_CTOR_DEF -> visitMethodDef(ast);
137
138 case TokenTypes.EXPR -> visitExpr();
139
140 case TokenTypes.BOR -> {
141 if (!isPipeOperator(ast) && !isPassedInParameter(ast)) {
142 context.visitBooleanOperator();
143 }
144 }
145
146 case TokenTypes.BAND,
147 TokenTypes.BXOR -> {
148 if (!isPassedInParameter(ast)) {
149 context.visitBooleanOperator();
150 }
151 }
152
153 case TokenTypes.LAND,
154 TokenTypes.LOR -> context.visitBooleanOperator();
155
156 default -> throw new IllegalArgumentException("Unknown type: " + ast);
157 }
158 }
159
160 /**
161 * Checks if logical operator is part of constructor or method call.
162 *
163 * @param logicalOperator logical operator
164 * @return true if logical operator is part of constructor or method call
165 */
166 private static boolean isPassedInParameter(DetailAST logicalOperator) {
167 return logicalOperator.getParent().getParent().getType() == TokenTypes.ELIST;
168 }
169
170 /**
171 * Checks if {@link TokenTypes#BOR binary OR} is applied to exceptions
172 * in
173 * <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-14.html#jls-14.20">
174 * multi-catch</a> (pipe-syntax).
175 *
176 * @param binaryOr {@link TokenTypes#BOR binary or}
177 * @return true if binary or is applied to exceptions in multi-catch.
178 */
179 private static boolean isPipeOperator(DetailAST binaryOr) {
180 return binaryOr.getParent().getType() == TokenTypes.TYPE;
181 }
182
183 @Override
184 public void leaveToken(DetailAST ast) {
185 switch (ast.getType()) {
186 case TokenTypes.CTOR_DEF,
187 TokenTypes.METHOD_DEF,
188 TokenTypes.COMPACT_CTOR_DEF -> leaveMethodDef();
189
190 case TokenTypes.EXPR -> leaveExpr(ast);
191
192 default -> {
193 // Do nothing
194 }
195 }
196 }
197
198 /**
199 * Creates new context for a given method.
200 *
201 * @param ast a method we start to check.
202 */
203 private void visitMethodDef(DetailAST ast) {
204 contextStack.push(context);
205 final boolean check = !CheckUtil.isEqualsMethod(ast);
206 context = new Context(check);
207 }
208
209 /** Removes old context. */
210 private void leaveMethodDef() {
211 context = contextStack.pop();
212 }
213
214 /** Creates and pushes new context. */
215 private void visitExpr() {
216 contextStack.push(context);
217 context = new Context(context.isChecking());
218 }
219
220 /**
221 * Restores previous context.
222 *
223 * @param ast expression we leave.
224 */
225 private void leaveExpr(DetailAST ast) {
226 context.checkCount(ast);
227 context = contextStack.pop();
228 }
229
230 /**
231 * Represents context (method/expression) in which we check complexity.
232 *
233 */
234 private final class Context {
235
236 /**
237 * Should we perform check in current context or not.
238 * Usually false if we are inside equals() method.
239 */
240 private final boolean checking;
241 /** Count of boolean operators. */
242 private int count;
243
244 /**
245 * Creates new instance.
246 *
247 * @param checking should we check in current context or not.
248 */
249 private Context(boolean checking) {
250 this.checking = checking;
251 }
252
253 /**
254 * Getter for checking property.
255 *
256 * @return should we check in current context or not.
257 */
258 public boolean isChecking() {
259 return checking;
260 }
261
262 /** Increases operator counter. */
263 public void visitBooleanOperator() {
264 ++count;
265 }
266
267 /**
268 * Checks if we violate maximum allowed complexity.
269 *
270 * @param ast a node we check now.
271 */
272 public void checkCount(DetailAST ast) {
273 if (checking && count > max) {
274 final DetailAST parentAST = ast.getParent();
275
276 log(parentAST, MSG_KEY, count, max);
277 }
278 }
279
280 }
281
282 }