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.math.BigInteger;
023import java.util.ArrayDeque;
024import java.util.Deque;
025
026import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
027import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
028import com.puppycrawl.tools.checkstyle.api.DetailAST;
029import com.puppycrawl.tools.checkstyle.api.TokenTypes;
030
031/**
032 * Checks cyclomatic complexity against a specified limit. The complexity is
033 * measured by the number of "if", "while", "do", "for", "?:", "catch",
034 * "switch", "case", "&&" and "||" statements (plus one) in the body of
035 * the member. It is a measure of the minimum number of possible paths through
036 * the source and therefore the number of required tests. Generally 1-4 is
037 * considered good, 5-7 ok, 8-10 consider re-factoring, and 11+ re-factor now!
038 *
039 * <p>Check has following properties:
040 *
041 * <p><b>switchBlockAsSingleDecisionPoint</b> - controls whether to treat the whole switch
042 * block as a single decision point. Default value is <b>false</b>
043 *
044 *
045 * @author <a href="mailto:simon@redhillconsulting.com.au">Simon Harris</a>
046 * @author Oliver Burn
047 * @author <a href="mailto:andreyselkin@gmail.com">Andrei Selkin</a>
048 */
049@FileStatefulCheck
050public class CyclomaticComplexityCheck
051    extends AbstractCheck {
052
053    /**
054     * A key is pointing to the warning message text in "messages.properties"
055     * file.
056     */
057    public static final String MSG_KEY = "cyclomaticComplexity";
058
059    /** The initial current value. */
060    private static final BigInteger INITIAL_VALUE = BigInteger.ONE;
061
062    /** Default allowed complexity. */
063    private static final int DEFAULT_COMPLEXITY_VALUE = 10;
064
065    /** Stack of values - all but the current value. */
066    private final Deque<BigInteger> valueStack = new ArrayDeque<>();
067
068    /** Whether to treat the whole switch block as a single decision point.*/
069    private boolean switchBlockAsSingleDecisionPoint;
070
071    /** The current value. */
072    private BigInteger currentValue = INITIAL_VALUE;
073
074    /** Threshold to report error for. */
075    private int max = DEFAULT_COMPLEXITY_VALUE;
076
077    /**
078     * Sets whether to treat the whole switch block as a single decision point.
079     * @param switchBlockAsSingleDecisionPoint whether to treat the whole switch
080     *                                          block as a single decision point.
081     */
082    public void setSwitchBlockAsSingleDecisionPoint(boolean switchBlockAsSingleDecisionPoint) {
083        this.switchBlockAsSingleDecisionPoint = switchBlockAsSingleDecisionPoint;
084    }
085
086    /**
087     * Set the maximum threshold allowed.
088     *
089     * @param max the maximum threshold
090     */
091    public final void setMax(int max) {
092        this.max = max;
093    }
094
095    @Override
096    public int[] getDefaultTokens() {
097        return new int[] {
098            TokenTypes.CTOR_DEF,
099            TokenTypes.METHOD_DEF,
100            TokenTypes.INSTANCE_INIT,
101            TokenTypes.STATIC_INIT,
102            TokenTypes.LITERAL_WHILE,
103            TokenTypes.LITERAL_DO,
104            TokenTypes.LITERAL_FOR,
105            TokenTypes.LITERAL_IF,
106            TokenTypes.LITERAL_SWITCH,
107            TokenTypes.LITERAL_CASE,
108            TokenTypes.LITERAL_CATCH,
109            TokenTypes.QUESTION,
110            TokenTypes.LAND,
111            TokenTypes.LOR,
112        };
113    }
114
115    @Override
116    public int[] getAcceptableTokens() {
117        return new int[] {
118            TokenTypes.CTOR_DEF,
119            TokenTypes.METHOD_DEF,
120            TokenTypes.INSTANCE_INIT,
121            TokenTypes.STATIC_INIT,
122            TokenTypes.LITERAL_WHILE,
123            TokenTypes.LITERAL_DO,
124            TokenTypes.LITERAL_FOR,
125            TokenTypes.LITERAL_IF,
126            TokenTypes.LITERAL_SWITCH,
127            TokenTypes.LITERAL_CASE,
128            TokenTypes.LITERAL_CATCH,
129            TokenTypes.QUESTION,
130            TokenTypes.LAND,
131            TokenTypes.LOR,
132        };
133    }
134
135    @Override
136    public final int[] getRequiredTokens() {
137        return new int[] {
138            TokenTypes.CTOR_DEF,
139            TokenTypes.METHOD_DEF,
140            TokenTypes.INSTANCE_INIT,
141            TokenTypes.STATIC_INIT,
142        };
143    }
144
145    @Override
146    public void visitToken(DetailAST ast) {
147        switch (ast.getType()) {
148            case TokenTypes.CTOR_DEF:
149            case TokenTypes.METHOD_DEF:
150            case TokenTypes.INSTANCE_INIT:
151            case TokenTypes.STATIC_INIT:
152                visitMethodDef();
153                break;
154            default:
155                visitTokenHook(ast);
156        }
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            case TokenTypes.INSTANCE_INIT:
165            case TokenTypes.STATIC_INIT:
166                leaveMethodDef(ast);
167                break;
168            default:
169                break;
170        }
171    }
172
173    /**
174     * Hook called when visiting a token. Will not be called the method
175     * definition tokens.
176     *
177     * @param ast the token being visited
178     */
179    private void visitTokenHook(DetailAST ast) {
180        if (switchBlockAsSingleDecisionPoint) {
181            if (ast.getType() != TokenTypes.LITERAL_CASE) {
182                incrementCurrentValue(BigInteger.ONE);
183            }
184        }
185        else if (ast.getType() != TokenTypes.LITERAL_SWITCH) {
186            incrementCurrentValue(BigInteger.ONE);
187        }
188    }
189
190    /**
191     * Process the end of a method definition.
192     *
193     * @param ast the token representing the method definition
194     */
195    private void leaveMethodDef(DetailAST ast) {
196        final BigInteger bigIntegerMax = BigInteger.valueOf(max);
197        if (currentValue.compareTo(bigIntegerMax) > 0) {
198            log(ast, MSG_KEY, currentValue, bigIntegerMax);
199        }
200        popValue();
201    }
202
203    /**
204     * Increments the current value by a specified amount.
205     *
206     * @param amount the amount to increment by
207     */
208    private void incrementCurrentValue(BigInteger amount) {
209        currentValue = currentValue.add(amount);
210    }
211
212    /** Push the current value on the stack. */
213    private void pushValue() {
214        valueStack.push(currentValue);
215        currentValue = INITIAL_VALUE;
216    }
217
218    /**
219     * Pops a value off the stack and makes it the current value.
220     */
221    private void popValue() {
222        currentValue = valueStack.pop();
223    }
224
225    /** Process the start of the method definition. */
226    private void visitMethodDef() {
227        pushValue();
228    }
229}