View Javadoc
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 &amp;&amp;}, {@code ||},
34   * {@code &amp;}, {@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 &amp;} 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 &amp;&amp;} and {@code ||}.
47   * </p>
48   *
49   * <p>
50   * Note that {@code &amp;}, {@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 }