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