View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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.math.BigInteger;
23  import java.util.ArrayDeque;
24  import java.util.Deque;
25  
26  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
27  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
28  import com.puppycrawl.tools.checkstyle.api.DetailAST;
29  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
30  
31  /**
32   * <p>
33   * Checks cyclomatic complexity against a specified limit. It is a measure of
34   * the minimum number of possible paths through the source and therefore the
35   * number of required tests, it is not about quality of code! It is only
36   * applied to methods, c-tors,
37   * <a href="https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html">
38   * static initializers and instance initializers</a>.
39   * </p>
40   * <p>
41   * The complexity is equal to the number of decision points {@code + 1}.
42   * Decision points:
43   * </p>
44   * <ul>
45   * <li>
46   * {@code if}, {@code while}, {@code do}, {@code for},
47   * {@code ?:}, {@code catch}, {@code switch}, {@code case} statements.
48   * </li>
49   * <li>
50   *  Operators {@code &amp;&amp;} and {@code ||} in the body of target.
51   * </li>
52   * <li>
53   *  {@code when} expression in case labels, also known as guards.
54   * </li>
55   * </ul>
56   * <p>
57   * By pure theory level 1-4 is considered easy to test, 5-7 OK, 8-10 consider
58   * re-factoring to ease testing, and 11+ re-factor now as testing will be painful.
59   * </p>
60   * <p>
61   * When it comes to code quality measurement by this metric level 10 is very
62   * good level as a ultimate target (that is hard to archive). Do not be ashamed
63   * to have complexity level 15 or even higher, but keep it below 20 to catch
64   * really bad-designed code automatically.
65   * </p>
66   * <p>
67   * Please use Suppression to avoid violations on cases that could not be split
68   * in few methods without damaging readability of code or encapsulation.
69   * </p>
70   * <ul>
71   * <li>
72   * Property {@code max} - Specify the maximum threshold allowed.
73   * Type is {@code int}.
74   * Default value is {@code 10}.
75   * </li>
76   * <li>
77   * Property {@code switchBlockAsSingleDecisionPoint} - Control whether to treat
78   * the whole switch block as a single decision point.
79   * Type is {@code boolean}.
80   * Default value is {@code false}.
81   * </li>
82   * <li>
83   * Property {@code tokens} - tokens to check
84   * Type is {@code java.lang.String[]}.
85   * Validation type is {@code tokenSet}.
86   * Default value is:
87   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_WHILE">
88   * LITERAL_WHILE</a>,
89   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_DO">
90   * LITERAL_DO</a>,
91   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_FOR">
92   * LITERAL_FOR</a>,
93   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_IF">
94   * LITERAL_IF</a>,
95   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_SWITCH">
96   * LITERAL_SWITCH</a>,
97   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_CASE">
98   * LITERAL_CASE</a>,
99   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_CATCH">
100  * LITERAL_CATCH</a>,
101  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#QUESTION">
102  * QUESTION</a>,
103  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LAND">
104  * LAND</a>,
105  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LOR">
106  * LOR</a>,
107  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_WHEN">
108  * LITERAL_WHEN</a>.
109  * </li>
110  * </ul>
111  * <p>
112  * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
113  * </p>
114  * <p>
115  * Violation Message Keys:
116  * </p>
117  * <ul>
118  * <li>
119  * {@code cyclomaticComplexity}
120  * </li>
121  * </ul>
122  *
123  * @since 3.2
124  */
125 @FileStatefulCheck
126 public class CyclomaticComplexityCheck
127     extends AbstractCheck {
128 
129     /**
130      * A key is pointing to the warning message text in "messages.properties"
131      * file.
132      */
133     public static final String MSG_KEY = "cyclomaticComplexity";
134 
135     /** The initial current value. */
136     private static final BigInteger INITIAL_VALUE = BigInteger.ONE;
137 
138     /** Default allowed complexity. */
139     private static final int DEFAULT_COMPLEXITY_VALUE = 10;
140 
141     /** Stack of values - all but the current value. */
142     private final Deque<BigInteger> valueStack = new ArrayDeque<>();
143 
144     /** Control whether to treat the whole switch block as a single decision point. */
145     private boolean switchBlockAsSingleDecisionPoint;
146 
147     /** The current value. */
148     private BigInteger currentValue = INITIAL_VALUE;
149 
150     /** Specify the maximum threshold allowed. */
151     private int max = DEFAULT_COMPLEXITY_VALUE;
152 
153     /**
154      * Setter to control whether to treat the whole switch block as a single decision point.
155      *
156      * @param switchBlockAsSingleDecisionPoint whether to treat the whole switch
157      *                                          block as a single decision point.
158      * @since 6.11
159      */
160     public void setSwitchBlockAsSingleDecisionPoint(boolean switchBlockAsSingleDecisionPoint) {
161         this.switchBlockAsSingleDecisionPoint = switchBlockAsSingleDecisionPoint;
162     }
163 
164     /**
165      * Setter to specify the maximum threshold allowed.
166      *
167      * @param max the maximum threshold
168      * @since 3.2
169      */
170     public final void setMax(int max) {
171         this.max = max;
172     }
173 
174     @Override
175     public int[] getDefaultTokens() {
176         return new int[] {
177             TokenTypes.CTOR_DEF,
178             TokenTypes.METHOD_DEF,
179             TokenTypes.INSTANCE_INIT,
180             TokenTypes.STATIC_INIT,
181             TokenTypes.LITERAL_WHILE,
182             TokenTypes.LITERAL_DO,
183             TokenTypes.LITERAL_FOR,
184             TokenTypes.LITERAL_IF,
185             TokenTypes.LITERAL_SWITCH,
186             TokenTypes.LITERAL_CASE,
187             TokenTypes.LITERAL_CATCH,
188             TokenTypes.QUESTION,
189             TokenTypes.LAND,
190             TokenTypes.LOR,
191             TokenTypes.COMPACT_CTOR_DEF,
192             TokenTypes.LITERAL_WHEN,
193         };
194     }
195 
196     @Override
197     public int[] getAcceptableTokens() {
198         return new int[] {
199             TokenTypes.CTOR_DEF,
200             TokenTypes.METHOD_DEF,
201             TokenTypes.INSTANCE_INIT,
202             TokenTypes.STATIC_INIT,
203             TokenTypes.LITERAL_WHILE,
204             TokenTypes.LITERAL_DO,
205             TokenTypes.LITERAL_FOR,
206             TokenTypes.LITERAL_IF,
207             TokenTypes.LITERAL_SWITCH,
208             TokenTypes.LITERAL_CASE,
209             TokenTypes.LITERAL_CATCH,
210             TokenTypes.QUESTION,
211             TokenTypes.LAND,
212             TokenTypes.LOR,
213             TokenTypes.COMPACT_CTOR_DEF,
214             TokenTypes.LITERAL_WHEN,
215         };
216     }
217 
218     @Override
219     public final int[] getRequiredTokens() {
220         return new int[] {
221             TokenTypes.CTOR_DEF,
222             TokenTypes.METHOD_DEF,
223             TokenTypes.INSTANCE_INIT,
224             TokenTypes.STATIC_INIT,
225             TokenTypes.COMPACT_CTOR_DEF,
226         };
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             case TokenTypes.INSTANCE_INIT:
235             case TokenTypes.STATIC_INIT:
236             case TokenTypes.COMPACT_CTOR_DEF:
237                 visitMethodDef();
238                 break;
239             default:
240                 visitTokenHook(ast);
241         }
242     }
243 
244     @Override
245     public void leaveToken(DetailAST ast) {
246         switch (ast.getType()) {
247             case TokenTypes.CTOR_DEF:
248             case TokenTypes.METHOD_DEF:
249             case TokenTypes.INSTANCE_INIT:
250             case TokenTypes.STATIC_INIT:
251             case TokenTypes.COMPACT_CTOR_DEF:
252                 leaveMethodDef(ast);
253                 break;
254             default:
255                 break;
256         }
257     }
258 
259     /**
260      * Hook called when visiting a token. Will not be called the method
261      * definition tokens.
262      *
263      * @param ast the token being visited
264      */
265     private void visitTokenHook(DetailAST ast) {
266         if (switchBlockAsSingleDecisionPoint) {
267             if (ast.getType() != TokenTypes.LITERAL_CASE) {
268                 incrementCurrentValue(BigInteger.ONE);
269             }
270         }
271         else if (ast.getType() != TokenTypes.LITERAL_SWITCH) {
272             incrementCurrentValue(BigInteger.ONE);
273         }
274     }
275 
276     /**
277      * Process the end of a method definition.
278      *
279      * @param ast the token representing the method definition
280      */
281     private void leaveMethodDef(DetailAST ast) {
282         final BigInteger bigIntegerMax = BigInteger.valueOf(max);
283         if (currentValue.compareTo(bigIntegerMax) > 0) {
284             log(ast, MSG_KEY, currentValue, bigIntegerMax);
285         }
286         popValue();
287     }
288 
289     /**
290      * Increments the current value by a specified amount.
291      *
292      * @param amount the amount to increment by
293      */
294     private void incrementCurrentValue(BigInteger amount) {
295         currentValue = currentValue.add(amount);
296     }
297 
298     /** Push the current value on the stack. */
299     private void pushValue() {
300         valueStack.push(currentValue);
301         currentValue = INITIAL_VALUE;
302     }
303 
304     /**
305      * Pops a value off the stack and makes it the current value.
306      */
307     private void popValue() {
308         currentValue = valueStack.pop();
309     }
310 
311     /** Process the start of the method definition. */
312     private void visitMethodDef() {
313         pushValue();
314     }
315 
316 }