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