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