001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2017 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.CheckUtils;
030
031/**
032 * Restricts nested boolean operators (&&, ||, &, | and ^) to
033 * a specified depth (default = 3).
034 * Note: &, | and ^ are not checked if they are part of constructor or
035 * method call because they can be applied to non boolean variables and
036 * Checkstyle does not know types of methods from different classes.
037 *
038 * @author <a href="mailto:simon@redhillconsulting.com.au">Simon Harris</a>
039 * @author o_sukhodolsky
040 */
041@FileStatefulCheck
042public final class BooleanExpressionComplexityCheck extends AbstractCheck {
043
044    /**
045     * A key is pointing to the warning message text in "messages.properties"
046     * file.
047     */
048    public static final String MSG_KEY = "booleanExpressionComplexity";
049
050    /** Default allowed complexity. */
051    private static final int DEFAULT_MAX = 3;
052
053    /** Stack of contexts. */
054    private final Deque<Context> contextStack = new ArrayDeque<>();
055    /** Maximum allowed complexity. */
056    private int max;
057    /** Current context. */
058    private Context context = new Context(false);
059
060    /** Creates new instance of the check. */
061    public BooleanExpressionComplexityCheck() {
062        max = DEFAULT_MAX;
063    }
064
065    @Override
066    public int[] getDefaultTokens() {
067        return new int[] {
068            TokenTypes.CTOR_DEF,
069            TokenTypes.METHOD_DEF,
070            TokenTypes.EXPR,
071            TokenTypes.LAND,
072            TokenTypes.BAND,
073            TokenTypes.LOR,
074            TokenTypes.BOR,
075            TokenTypes.BXOR,
076        };
077    }
078
079    @Override
080    public int[] getRequiredTokens() {
081        return new int[] {
082            TokenTypes.CTOR_DEF,
083            TokenTypes.METHOD_DEF,
084            TokenTypes.EXPR,
085        };
086    }
087
088    @Override
089    public int[] getAcceptableTokens() {
090        return new int[] {
091            TokenTypes.CTOR_DEF,
092            TokenTypes.METHOD_DEF,
093            TokenTypes.EXPR,
094            TokenTypes.LAND,
095            TokenTypes.BAND,
096            TokenTypes.LOR,
097            TokenTypes.BOR,
098            TokenTypes.BXOR,
099        };
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         * Should we perform check in current context or not.
216         * Usually false if we are inside equals() method.
217         */
218        private final boolean checking;
219        /** Count of boolean operators. */
220        private int count;
221
222        /**
223         * Creates new instance.
224         * @param checking should we check in current context or not.
225         */
226        Context(boolean checking) {
227            this.checking = checking;
228            count = 0;
229        }
230
231        /**
232         * Getter for checking property.
233         * @return should we check in current context or not.
234         */
235        public boolean isChecking() {
236            return checking;
237        }
238
239        /** Increases operator counter. */
240        public void visitBooleanOperator() {
241            ++count;
242        }
243
244        /**
245         * Checks if we violates maximum allowed complexity.
246         * @param ast a node we check now.
247         */
248        public void checkCount(DetailAST ast) {
249            if (checking && count > max) {
250                final DetailAST parentAST = ast.getParent();
251
252                log(parentAST.getLineNo(), parentAST.getColumnNo(),
253                    MSG_KEY, count, max);
254            }
255        }
256    }
257}