001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2020 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.metrics;
021
022import java.util.ArrayDeque;
023import java.util.Deque;
024
025import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
026import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
027import com.puppycrawl.tools.checkstyle.api.DetailAST;
028import com.puppycrawl.tools.checkstyle.api.TokenTypes;
029import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
030
031/**
032 * <p>
033 * Restricts the number of boolean operators ({@code &amp;&amp;}, {@code ||},
034 * {@code &amp;}, {@code |} and {@code ^}) in an expression.
035 * </p>
036 * <p>
037 * Rationale: Too many conditions leads to code that is difficult to read
038 * and hence debug and maintain.
039 * </p>
040 * <p>
041 * Note that the operators {@code &amp;} and {@code |} are not only integer bitwise
042 * operators, they are also the
043 * <a href="https://docs.oracle.com/javase/specs/jls/se11/html/jls-15.html#jls-15.22.2">
044 * non-shortcut versions</a> of the boolean operators {@code &amp;&amp;} and {@code ||}.
045 * </p>
046 * <p>
047 * Note that {@code &amp;}, {@code |} and {@code ^} are not checked if they are part
048 * of constructor or method call because they can be applied to non boolean
049 * variables and Checkstyle does not know types of methods from different classes.
050 * </p>
051 * <ul>
052 * <li>
053 * Property {@code max} - Specify the maximum number of boolean operations
054 * allowed in one expression.
055 * Type is {@code int}.
056 * Default value is {@code 3}.
057 * </li>
058 * <li>
059 * Property {@code tokens} - tokens to check
060 * Type is {@code int[]}.
061 * Default value is:
062 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LAND">
063 * LAND</a>,
064 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#BAND">
065 * BAND</a>,
066 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LOR">
067 * LOR</a>,
068 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#BOR">
069 * BOR</a>,
070 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#BXOR">
071 * BXOR</a>.
072 * </li>
073 * </ul>
074 * <p>
075 * To configure the check:
076 * </p>
077 * <pre>
078 * &lt;module name="BooleanExpressionComplexity"/&gt;
079 * </pre>
080 * <p>Code Example:</p>
081 * <pre>
082 * public class Test
083 * {
084 * public static void main(String ... args)
085 * {
086 * boolean a = true;
087 * boolean b = false;
088 *
089 * boolean c = (a &amp; b) | (b ^ a);       // OK, 1(&amp;) + 1(|) + 1(^) = 3 (max allowed 3)
090 *
091 * boolean d = (a &amp; b) ^ (a || b) | a;  // violation, 1(&amp;) + 1(^) + 1(||) + 1(|) = 4
092 * }
093 * }
094 * </pre>
095 * <p>
096 * To configure the check with 5 allowed operation in boolean expression:
097 * </p>
098 * <pre>
099 * &lt;module name="BooleanExpressionComplexity"&gt;
100 *   &lt;property name="max" value="5"/&gt;
101 * &lt;/module&gt;
102 * </pre>
103 * <p>Code Example:</p>
104 * <pre>
105 * public class Test
106 * {
107 *  public static void main(String ... args)
108 *  {
109 *   boolean a = true;
110 *   boolean b = false;
111 *
112 *   boolean c = (a &amp; b) | (b ^ a) | (a ^ b);   // OK, 1(&amp;) + 1(|) + 1(^) + 1(|) + 1(^) = 5
113 *
114 *   boolean d = (a | b) ^ (a | b) ^ (a || b) &amp; b; // violation,
115 *                                               // 1(|) + 1(^) + 1(|) + 1(^) + 1(||) + 1(&amp;) = 6
116 *  }
117 * }
118 * </pre>
119 * <p>
120 * To configure the check to ignore {@code &amp;} and {@code |}:
121 * </p>
122 * <pre>
123 * &lt;module name="BooleanExpressionComplexity"&gt;
124 *   &lt;property name="tokens" value="BXOR,LAND,LOR"/&gt;
125 * &lt;/module&gt;
126 * </pre>
127 * <p>Code Example:</p>
128 * <pre>
129 * public class Test
130 * {
131 *  public static void main(String ... args)
132 *   {
133 *     boolean a = true;
134 *     boolean b = false;
135 *
136 *     boolean c = (!a &amp;&amp; b) | (a || !b) ^ a;    // OK, 1(&amp;&amp;) + 1(||) + 1(^) = 3
137 *                                                // | is ignored here
138 *
139 *     boolean d = a ^ (a || b) ^ (b || a) &amp; a; // violation, 1(^) + 1(||) + 1(^) + 1(||) = 4
140 *                                               // &amp; is ignored here
141 *    }
142 *  }
143 * </pre>
144 *
145 * <p>
146 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
147 * </p>
148 * <p>
149 * Violation Message Keys:
150 * </p>
151 * <ul>
152 * <li>
153 * {@code booleanExpressionComplexity}
154 * </li>
155 * </ul>
156 *
157 * @since 3.4
158 */
159@FileStatefulCheck
160public final class BooleanExpressionComplexityCheck extends AbstractCheck {
161
162    /**
163     * A key is pointing to the warning message text in "messages.properties"
164     * file.
165     */
166    public static final String MSG_KEY = "booleanExpressionComplexity";
167
168    /** Default allowed complexity. */
169    private static final int DEFAULT_MAX = 3;
170
171    /** Stack of contexts. */
172    private final Deque<Context> contextStack = new ArrayDeque<>();
173    /** Specify the maximum number of boolean operations allowed in one expression. */
174    private int max;
175    /** Current context. */
176    private Context context = new Context(false);
177
178    /** Creates new instance of the check. */
179    public BooleanExpressionComplexityCheck() {
180        max = DEFAULT_MAX;
181    }
182
183    @Override
184    public int[] getDefaultTokens() {
185        return new int[] {
186            TokenTypes.CTOR_DEF,
187            TokenTypes.METHOD_DEF,
188            TokenTypes.EXPR,
189            TokenTypes.LAND,
190            TokenTypes.BAND,
191            TokenTypes.LOR,
192            TokenTypes.BOR,
193            TokenTypes.BXOR,
194        };
195    }
196
197    @Override
198    public int[] getRequiredTokens() {
199        return new int[] {
200            TokenTypes.CTOR_DEF,
201            TokenTypes.METHOD_DEF,
202            TokenTypes.EXPR,
203        };
204    }
205
206    @Override
207    public int[] getAcceptableTokens() {
208        return new int[] {
209            TokenTypes.CTOR_DEF,
210            TokenTypes.METHOD_DEF,
211            TokenTypes.EXPR,
212            TokenTypes.LAND,
213            TokenTypes.BAND,
214            TokenTypes.LOR,
215            TokenTypes.BOR,
216            TokenTypes.BXOR,
217        };
218    }
219
220    /**
221     * Setter to specify the maximum number of boolean operations allowed in one expression.
222     *
223     * @param max new maximum allowed complexity.
224     */
225    public void setMax(int max) {
226        this.max = max;
227    }
228
229    @Override
230    public void visitToken(DetailAST ast) {
231        switch (ast.getType()) {
232            case TokenTypes.CTOR_DEF:
233            case TokenTypes.METHOD_DEF:
234                visitMethodDef(ast);
235                break;
236            case TokenTypes.EXPR:
237                visitExpr();
238                break;
239            case TokenTypes.BOR:
240                if (!isPipeOperator(ast) && !isPassedInParameter(ast)) {
241                    context.visitBooleanOperator();
242                }
243                break;
244            case TokenTypes.BAND:
245            case TokenTypes.BXOR:
246                if (!isPassedInParameter(ast)) {
247                    context.visitBooleanOperator();
248                }
249                break;
250            case TokenTypes.LAND:
251            case TokenTypes.LOR:
252                context.visitBooleanOperator();
253                break;
254            default:
255                throw new IllegalArgumentException("Unknown type: " + ast);
256        }
257    }
258
259    /**
260     * Checks if logical operator is part of constructor or method call.
261     *
262     * @param logicalOperator logical operator
263     * @return true if logical operator is part of constructor or method call
264     */
265    private static boolean isPassedInParameter(DetailAST logicalOperator) {
266        return logicalOperator.getParent().getParent().getType() == TokenTypes.ELIST;
267    }
268
269    /**
270     * Checks if {@link TokenTypes#BOR binary OR} is applied to exceptions
271     * in
272     * <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-14.html#jls-14.20">
273     * multi-catch</a> (pipe-syntax).
274     *
275     * @param binaryOr {@link TokenTypes#BOR binary or}
276     * @return true if binary or is applied to exceptions in multi-catch.
277     */
278    private static boolean isPipeOperator(DetailAST binaryOr) {
279        return binaryOr.getParent().getType() == TokenTypes.TYPE;
280    }
281
282    @Override
283    public void leaveToken(DetailAST ast) {
284        switch (ast.getType()) {
285            case TokenTypes.CTOR_DEF:
286            case TokenTypes.METHOD_DEF:
287                leaveMethodDef();
288                break;
289            case TokenTypes.EXPR:
290                leaveExpr(ast);
291                break;
292            default:
293                // Do nothing
294        }
295    }
296
297    /**
298     * Creates new context for a given method.
299     *
300     * @param ast a method we start to check.
301     */
302    private void visitMethodDef(DetailAST ast) {
303        contextStack.push(context);
304        final boolean check = !CheckUtil.isEqualsMethod(ast);
305        context = new Context(check);
306    }
307
308    /** Removes old context. */
309    private void leaveMethodDef() {
310        context = contextStack.pop();
311    }
312
313    /** Creates and pushes new context. */
314    private void visitExpr() {
315        contextStack.push(context);
316        context = new Context(context.isChecking());
317    }
318
319    /**
320     * Restores previous context.
321     *
322     * @param ast expression we leave.
323     */
324    private void leaveExpr(DetailAST ast) {
325        context.checkCount(ast);
326        context = contextStack.pop();
327    }
328
329    /**
330     * Represents context (method/expression) in which we check complexity.
331     *
332     */
333    private class Context {
334
335        /**
336         * Should we perform check in current context or not.
337         * Usually false if we are inside equals() method.
338         */
339        private final boolean checking;
340        /** Count of boolean operators. */
341        private int count;
342
343        /**
344         * Creates new instance.
345         *
346         * @param checking should we check in current context or not.
347         */
348        /* package */ Context(boolean checking) {
349            this.checking = checking;
350            count = 0;
351        }
352
353        /**
354         * Getter for checking property.
355         *
356         * @return should we check in current context or not.
357         */
358        public boolean isChecking() {
359            return checking;
360        }
361
362        /** Increases operator counter. */
363        public void visitBooleanOperator() {
364            ++count;
365        }
366
367        /**
368         * Checks if we violates maximum allowed complexity.
369         *
370         * @param ast a node we check now.
371         */
372        public void checkCount(DetailAST ast) {
373            if (checking && count > max) {
374                final DetailAST parentAST = ast.getParent();
375
376                log(parentAST, MSG_KEY, count, max);
377            }
378        }
379
380    }
381
382}