001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 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 * <p>
033 * Checks cyclomatic complexity against a specified limit. It is a measure of
034 * the minimum number of possible paths through the source and therefore the
035 * number of required tests, it is not about quality of code! It is only
036 * applied to methods, c-tors,
037 * <a href="https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html">
038 * static initializers and instance initializers</a>.
039 * </p>
040 * <p>
041 * The complexity is equal to the number of decision points {@code + 1}.
042 * Decision points: {@code if}, {@code while}, {@code do}, {@code for},
043 * {@code ?:}, {@code catch}, {@code switch}, {@code case} statements and
044 * operators {@code &amp;&amp;} and {@code ||} in the body of target.
045 * </p>
046 * <p>
047 * By pure theory level 1-4 is considered easy to test, 5-7 OK, 8-10 consider
048 * re-factoring to ease testing, and 11+ re-factor now as testing will be painful.
049 * </p>
050 * <p>
051 * When it comes to code quality measurement by this metric level 10 is very
052 * good level as a ultimate target (that is hard to archive). Do not be ashamed
053 * to have complexity level 15 or even higher, but keep it below 20 to catch
054 * really bad-designed code automatically.
055 * </p>
056 * <p>
057 * Please use Suppression to avoid violations on cases that could not be split
058 * in few methods without damaging readability of code or encapsulation.
059 * </p>
060 * <ul>
061 * <li>
062 * Property {@code max} - Specify the maximum threshold allowed.
063 * Type is {@code int}.
064 * Default value is {@code 10}.
065 * </li>
066 * <li>
067 * Property {@code switchBlockAsSingleDecisionPoint} - Control whether to treat
068 * the whole switch block as a single decision point.
069 * Type is {@code boolean}.
070 * Default value is {@code false}.
071 * </li>
072 * <li>
073 * Property {@code tokens} - tokens to check
074 * Type is {@code java.lang.String[]}.
075 * Validation type is {@code tokenSet}.
076 * Default value is:
077 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_WHILE">
078 * LITERAL_WHILE</a>,
079 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_DO">
080 * LITERAL_DO</a>,
081 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_FOR">
082 * LITERAL_FOR</a>,
083 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_IF">
084 * LITERAL_IF</a>,
085 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_SWITCH">
086 * LITERAL_SWITCH</a>,
087 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_CASE">
088 * LITERAL_CASE</a>,
089 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_CATCH">
090 * LITERAL_CATCH</a>,
091 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#QUESTION">
092 * QUESTION</a>,
093 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LAND">
094 * LAND</a>,
095 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LOR">
096 * LOR</a>.
097 * </li>
098 * </ul>
099 * <p>
100 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
101 * </p>
102 * <p>
103 * Violation Message Keys:
104 * </p>
105 * <ul>
106 * <li>
107 * {@code cyclomaticComplexity}
108 * </li>
109 * </ul>
110 *
111 * @since 3.2
112 */
113@FileStatefulCheck
114public class CyclomaticComplexityCheck
115    extends AbstractCheck {
116
117    /**
118     * A key is pointing to the warning message text in "messages.properties"
119     * file.
120     */
121    public static final String MSG_KEY = "cyclomaticComplexity";
122
123    /** The initial current value. */
124    private static final BigInteger INITIAL_VALUE = BigInteger.ONE;
125
126    /** Default allowed complexity. */
127    private static final int DEFAULT_COMPLEXITY_VALUE = 10;
128
129    /** Stack of values - all but the current value. */
130    private final Deque<BigInteger> valueStack = new ArrayDeque<>();
131
132    /** Control whether to treat the whole switch block as a single decision point. */
133    private boolean switchBlockAsSingleDecisionPoint;
134
135    /** The current value. */
136    private BigInteger currentValue = INITIAL_VALUE;
137
138    /** Specify the maximum threshold allowed. */
139    private int max = DEFAULT_COMPLEXITY_VALUE;
140
141    /**
142     * Setter to control whether to treat the whole switch block as a single decision point.
143     *
144     * @param switchBlockAsSingleDecisionPoint whether to treat the whole switch
145     *                                          block as a single decision point.
146     * @since 6.11
147     */
148    public void setSwitchBlockAsSingleDecisionPoint(boolean switchBlockAsSingleDecisionPoint) {
149        this.switchBlockAsSingleDecisionPoint = switchBlockAsSingleDecisionPoint;
150    }
151
152    /**
153     * Setter to specify the maximum threshold allowed.
154     *
155     * @param max the maximum threshold
156     * @since 3.2
157     */
158    public final void setMax(int max) {
159        this.max = max;
160    }
161
162    @Override
163    public int[] getDefaultTokens() {
164        return new int[] {
165            TokenTypes.CTOR_DEF,
166            TokenTypes.METHOD_DEF,
167            TokenTypes.INSTANCE_INIT,
168            TokenTypes.STATIC_INIT,
169            TokenTypes.LITERAL_WHILE,
170            TokenTypes.LITERAL_DO,
171            TokenTypes.LITERAL_FOR,
172            TokenTypes.LITERAL_IF,
173            TokenTypes.LITERAL_SWITCH,
174            TokenTypes.LITERAL_CASE,
175            TokenTypes.LITERAL_CATCH,
176            TokenTypes.QUESTION,
177            TokenTypes.LAND,
178            TokenTypes.LOR,
179            TokenTypes.COMPACT_CTOR_DEF,
180        };
181    }
182
183    @Override
184    public int[] getAcceptableTokens() {
185        return new int[] {
186            TokenTypes.CTOR_DEF,
187            TokenTypes.METHOD_DEF,
188            TokenTypes.INSTANCE_INIT,
189            TokenTypes.STATIC_INIT,
190            TokenTypes.LITERAL_WHILE,
191            TokenTypes.LITERAL_DO,
192            TokenTypes.LITERAL_FOR,
193            TokenTypes.LITERAL_IF,
194            TokenTypes.LITERAL_SWITCH,
195            TokenTypes.LITERAL_CASE,
196            TokenTypes.LITERAL_CATCH,
197            TokenTypes.QUESTION,
198            TokenTypes.LAND,
199            TokenTypes.LOR,
200            TokenTypes.COMPACT_CTOR_DEF,
201        };
202    }
203
204    @Override
205    public final int[] getRequiredTokens() {
206        return new int[] {
207            TokenTypes.CTOR_DEF,
208            TokenTypes.METHOD_DEF,
209            TokenTypes.INSTANCE_INIT,
210            TokenTypes.STATIC_INIT,
211            TokenTypes.COMPACT_CTOR_DEF,
212        };
213    }
214
215    @Override
216    public void visitToken(DetailAST ast) {
217        switch (ast.getType()) {
218            case TokenTypes.CTOR_DEF:
219            case TokenTypes.METHOD_DEF:
220            case TokenTypes.INSTANCE_INIT:
221            case TokenTypes.STATIC_INIT:
222            case TokenTypes.COMPACT_CTOR_DEF:
223                visitMethodDef();
224                break;
225            default:
226                visitTokenHook(ast);
227        }
228    }
229
230    @Override
231    public void leaveToken(DetailAST ast) {
232        switch (ast.getType()) {
233            case TokenTypes.CTOR_DEF:
234            case TokenTypes.METHOD_DEF:
235            case TokenTypes.INSTANCE_INIT:
236            case TokenTypes.STATIC_INIT:
237            case TokenTypes.COMPACT_CTOR_DEF:
238                leaveMethodDef(ast);
239                break;
240            default:
241                break;
242        }
243    }
244
245    /**
246     * Hook called when visiting a token. Will not be called the method
247     * definition tokens.
248     *
249     * @param ast the token being visited
250     */
251    private void visitTokenHook(DetailAST ast) {
252        if (switchBlockAsSingleDecisionPoint) {
253            if (ast.getType() != TokenTypes.LITERAL_CASE) {
254                incrementCurrentValue(BigInteger.ONE);
255            }
256        }
257        else if (ast.getType() != TokenTypes.LITERAL_SWITCH) {
258            incrementCurrentValue(BigInteger.ONE);
259        }
260    }
261
262    /**
263     * Process the end of a method definition.
264     *
265     * @param ast the token representing the method definition
266     */
267    private void leaveMethodDef(DetailAST ast) {
268        final BigInteger bigIntegerMax = BigInteger.valueOf(max);
269        if (currentValue.compareTo(bigIntegerMax) > 0) {
270            log(ast, MSG_KEY, currentValue, bigIntegerMax);
271        }
272        popValue();
273    }
274
275    /**
276     * Increments the current value by a specified amount.
277     *
278     * @param amount the amount to increment by
279     */
280    private void incrementCurrentValue(BigInteger amount) {
281        currentValue = currentValue.add(amount);
282    }
283
284    /** Push the current value on the stack. */
285    private void pushValue() {
286        valueStack.push(currentValue);
287        currentValue = INITIAL_VALUE;
288    }
289
290    /**
291     * Pops a value off the stack and makes it the current value.
292     */
293    private void popValue() {
294        currentValue = valueStack.pop();
295    }
296
297    /** Process the start of the method definition. */
298    private void visitMethodDef() {
299        pushValue();
300    }
301
302}