1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 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   * Checks the npath complexity against a specified limit (default = 200).
33   * The npath metric computes the number of possible execution paths
34   * through a function. Similar to the cyclomatic complexity but also
35   * takes into account the nesting of conditional statements and
36   * multi-part boolean expressions.
37   *
38   */
39  // -@cs[AbbreviationAsWordInName] Can't change check name
40  @FileStatefulCheck
41  public final class NPathComplexityCheck extends AbstractCheck {
42  
43      /**
44       * A key is pointing to the warning message text in "messages.properties"
45       * file.
46       */
47      public static final String MSG_KEY = "npathComplexity";
48  
49      /** Default allowed complexity. */
50      private static final int DEFAULT_MAX = 200;
51  
52      /** The initial current value. */
53      private static final BigInteger INITIAL_VALUE = BigInteger.ZERO;
54  
55      /**
56       * Stack of NP values for ranges.
57       */
58      private final Deque<BigInteger> rangeValues = new ArrayDeque<>();
59  
60      /** Stack of NP values for expressions. */
61      private final Deque<Integer> expressionValues = new ArrayDeque<>();
62  
63      /** Stack of belongs to range values for question operator. */
64      private final Deque<Boolean> afterValues = new ArrayDeque<>();
65  
66      /**
67       * Range of the last processed expression. Used for checking that ternary operation
68       * which is a part of expression won't be processed for second time.
69       */
70      private final TokenEnd processingTokenEnd = new TokenEnd();
71  
72      /** NP value for current range. */
73      private BigInteger currentRangeValue = INITIAL_VALUE;
74  
75      /** Threshold to report error for. */
76      private int max = DEFAULT_MAX;
77  
78      /** True, when branch is visited, but not leaved. */
79      private boolean branchVisited;
80  
81      /**
82       * Set the maximum threshold allowed.
83       * @param max the maximum threshold
84       */
85      public void setMax(int max) {
86          this.max = max;
87      }
88  
89      @Override
90      public int[] getDefaultTokens() {
91          return getRequiredTokens();
92      }
93  
94      @Override
95      public int[] getAcceptableTokens() {
96          return getRequiredTokens();
97      }
98  
99      @Override
100     public int[] getRequiredTokens() {
101         return new int[] {
102             TokenTypes.CTOR_DEF,
103             TokenTypes.METHOD_DEF,
104             TokenTypes.STATIC_INIT,
105             TokenTypes.INSTANCE_INIT,
106             TokenTypes.LITERAL_WHILE,
107             TokenTypes.LITERAL_DO,
108             TokenTypes.LITERAL_FOR,
109             TokenTypes.LITERAL_IF,
110             TokenTypes.LITERAL_ELSE,
111             TokenTypes.LITERAL_SWITCH,
112             TokenTypes.CASE_GROUP,
113             TokenTypes.LITERAL_TRY,
114             TokenTypes.LITERAL_CATCH,
115             TokenTypes.QUESTION,
116             TokenTypes.LITERAL_RETURN,
117             TokenTypes.LITERAL_DEFAULT,
118         };
119     }
120 
121     @Override
122     public void beginTree(DetailAST rootAST) {
123         rangeValues.clear();
124         expressionValues.clear();
125         afterValues.clear();
126         processingTokenEnd.reset();
127         currentRangeValue = INITIAL_VALUE;
128         branchVisited = false;
129     }
130 
131     @Override
132     public void visitToken(DetailAST ast) {
133         switch (ast.getType()) {
134             case TokenTypes.LITERAL_IF:
135             case TokenTypes.LITERAL_SWITCH:
136             case TokenTypes.LITERAL_WHILE:
137             case TokenTypes.LITERAL_DO:
138             case TokenTypes.LITERAL_FOR:
139                 visitConditional(ast, 1);
140                 break;
141             case TokenTypes.QUESTION:
142                 visitUnitaryOperator(ast, 2);
143                 break;
144             case TokenTypes.LITERAL_RETURN:
145                 visitUnitaryOperator(ast, 0);
146                 break;
147             case TokenTypes.CASE_GROUP:
148                 final int caseNumber = countCaseTokens(ast);
149                 branchVisited = true;
150                 pushValue(caseNumber);
151                 break;
152             case TokenTypes.LITERAL_ELSE:
153                 branchVisited = true;
154                 if (currentRangeValue.equals(BigInteger.ZERO)) {
155                     currentRangeValue = BigInteger.ONE;
156                 }
157                 pushValue(0);
158                 break;
159             case TokenTypes.LITERAL_TRY:
160             case TokenTypes.LITERAL_CATCH:
161             case TokenTypes.LITERAL_DEFAULT:
162                 pushValue(1);
163                 break;
164             case TokenTypes.CTOR_DEF:
165             case TokenTypes.METHOD_DEF:
166             case TokenTypes.INSTANCE_INIT:
167             case TokenTypes.STATIC_INIT:
168                 pushValue(0);
169                 break;
170             default:
171                 break;
172         }
173     }
174 
175     @Override
176     public void leaveToken(DetailAST ast) {
177         switch (ast.getType()) {
178             case TokenTypes.LITERAL_WHILE:
179             case TokenTypes.LITERAL_DO:
180             case TokenTypes.LITERAL_FOR:
181             case TokenTypes.LITERAL_IF:
182             case TokenTypes.LITERAL_SWITCH:
183                 leaveConditional();
184                 break;
185             case TokenTypes.LITERAL_TRY:
186                 leaveMultiplyingConditional();
187                 break;
188             case TokenTypes.LITERAL_RETURN:
189             case TokenTypes.QUESTION:
190                 leaveUnitaryOperator();
191                 break;
192             case TokenTypes.LITERAL_CATCH:
193                 leaveAddingConditional();
194                 break;
195             case TokenTypes.LITERAL_DEFAULT:
196                 leaveBranch();
197                 break;
198             case TokenTypes.LITERAL_ELSE:
199             case TokenTypes.CASE_GROUP:
200                 leaveBranch();
201                 branchVisited = false;
202                 break;
203             case TokenTypes.CTOR_DEF:
204             case TokenTypes.METHOD_DEF:
205             case TokenTypes.INSTANCE_INIT:
206             case TokenTypes.STATIC_INIT:
207                 leaveMethodDef(ast);
208                 break;
209             default:
210                 break;
211         }
212     }
213 
214     /**
215      * Visits if, while, do-while, for and switch tokens - all of them have expression in
216      * parentheses which is used for calculation.
217      * @param ast visited token.
218      * @param basicBranchingFactor default number of branches added.
219      */
220     private void visitConditional(DetailAST ast, int basicBranchingFactor) {
221         int expressionValue = basicBranchingFactor;
222         DetailAST bracketed;
223         for (bracketed = ast.findFirstToken(TokenTypes.LPAREN).getNextSibling();
224                 bracketed.getType() != TokenTypes.RPAREN;
225                 bracketed = bracketed.getNextSibling()) {
226             expressionValue += countConditionalOperators(bracketed);
227         }
228         processingTokenEnd.setToken(bracketed);
229         pushValue(expressionValue);
230     }
231 
232     /**
233      * Visits ternary operator (?:) and return tokens. They differ from those processed by
234      * visitConditional method in that their expression isn't bracketed.
235      * @param ast visited token.
236      * @param basicBranchingFactor number of branches inherently added by this token.
237      */
238     private void visitUnitaryOperator(DetailAST ast, int basicBranchingFactor) {
239         final boolean isAfter = processingTokenEnd.isAfter(ast);
240         afterValues.push(isAfter);
241         if (!isAfter) {
242             processingTokenEnd.setToken(getLastToken(ast));
243             final int expressionValue = basicBranchingFactor + countConditionalOperators(ast);
244             pushValue(expressionValue);
245         }
246     }
247 
248     /**
249      * Leaves ternary operator (?:) and return tokens.
250      */
251     private void leaveUnitaryOperator() {
252         if (!afterValues.pop()) {
253             final Values valuePair = popValue();
254             BigInteger basicRangeValue = valuePair.getRangeValue();
255             BigInteger expressionValue = valuePair.getExpressionValue();
256             if (expressionValue.equals(BigInteger.ZERO)) {
257                 expressionValue = BigInteger.ONE;
258             }
259             if (basicRangeValue.equals(BigInteger.ZERO)) {
260                 basicRangeValue = BigInteger.ONE;
261             }
262             currentRangeValue = currentRangeValue.add(expressionValue).multiply(basicRangeValue);
263         }
264     }
265 
266     /** Leaves while, do, for, if, ternary (?::), return or switch. */
267     private void leaveConditional() {
268         final Values valuePair = popValue();
269         final BigInteger expressionValue = valuePair.getExpressionValue();
270         BigInteger basicRangeValue = valuePair.getRangeValue();
271         if (currentRangeValue.equals(BigInteger.ZERO)) {
272             currentRangeValue = BigInteger.ONE;
273         }
274         if (basicRangeValue.equals(BigInteger.ZERO)) {
275             basicRangeValue = BigInteger.ONE;
276         }
277         currentRangeValue = currentRangeValue.add(expressionValue).multiply(basicRangeValue);
278     }
279 
280     /** Leaves else, default or case group tokens. */
281     private void leaveBranch() {
282         final Values valuePair = popValue();
283         final BigInteger basicRangeValue = valuePair.getRangeValue();
284         final BigInteger expressionValue = valuePair.getExpressionValue();
285         if (branchVisited && currentRangeValue.equals(BigInteger.ZERO)) {
286             currentRangeValue = BigInteger.ONE;
287         }
288         currentRangeValue = currentRangeValue.subtract(BigInteger.ONE)
289                 .add(basicRangeValue)
290                 .add(expressionValue);
291     }
292 
293     /**
294      * Process the end of a method definition.
295      * @param ast the token type representing the method definition
296      */
297     private void leaveMethodDef(DetailAST ast) {
298         final BigInteger bigIntegerMax = BigInteger.valueOf(max);
299         if (currentRangeValue.compareTo(bigIntegerMax) > 0) {
300             log(ast, MSG_KEY, currentRangeValue, bigIntegerMax);
301         }
302         popValue();
303         currentRangeValue = INITIAL_VALUE;
304     }
305 
306     /** Leaves catch. */
307     private void leaveAddingConditional() {
308         currentRangeValue = currentRangeValue.add(popValue().getRangeValue().add(BigInteger.ONE));
309     }
310 
311     /**
312      * Pushes the current range value on the range value stack. Pushes this token expression value
313      * on the expression value stack.
314      * @param expressionValue value of expression calculated for current token.
315      */
316     private void pushValue(Integer expressionValue) {
317         rangeValues.push(currentRangeValue);
318         expressionValues.push(expressionValue);
319         currentRangeValue = INITIAL_VALUE;
320     }
321 
322     /**
323      * Pops values from both stack of expression values and stack of range values.
324      * @return pair of head values from both of the stacks.
325      */
326     private Values popValue() {
327         final int expressionValue = expressionValues.pop();
328         return new Values(rangeValues.pop(), BigInteger.valueOf(expressionValue));
329     }
330 
331     /** Leaves try. */
332     private void leaveMultiplyingConditional() {
333         currentRangeValue = currentRangeValue.add(BigInteger.ONE)
334                 .multiply(popValue().getRangeValue().add(BigInteger.ONE));
335     }
336 
337     /**
338      * Calculates number of conditional operators, including inline ternary operator, for a token.
339      * @param ast inspected token.
340      * @return number of conditional operators.
341      * @see <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.23">
342      * Java Language Specification, &sect;15.23</a>
343      * @see <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.24">
344      * Java Language Specification, &sect;15.24</a>
345      * @see <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.25">
346      * Java Language Specification, &sect;15.25</a>
347      */
348     private static int countConditionalOperators(DetailAST ast) {
349         int number = 0;
350         for (DetailAST child = ast.getFirstChild(); child != null;
351                 child = child.getNextSibling()) {
352             final int type = child.getType();
353             if (type == TokenTypes.LOR || type == TokenTypes.LAND) {
354                 number++;
355             }
356             else if (type == TokenTypes.QUESTION) {
357                 number += 2;
358             }
359             number += countConditionalOperators(child);
360         }
361         return number;
362     }
363 
364     /**
365      * Finds a leaf, which is the most distant from the root.
366      * @param ast the root of tree.
367      * @return the leaf.
368      */
369     private static DetailAST getLastToken(DetailAST ast) {
370         final DetailAST lastChild = ast.getLastChild();
371         final DetailAST result;
372         if (lastChild.getFirstChild() == null) {
373             result = lastChild;
374         }
375         else {
376             result = getLastToken(lastChild);
377         }
378         return result;
379     }
380 
381     /**
382      * Counts number of case tokens subject to a case group token.
383      * @param ast case group token.
384      * @return number of case tokens.
385      */
386     private static int countCaseTokens(DetailAST ast) {
387         int counter = 0;
388         for (DetailAST iterator = ast.getFirstChild(); iterator != null;
389                 iterator = iterator.getNextSibling()) {
390             if (iterator.getType() == TokenTypes.LITERAL_CASE) {
391                 counter++;
392             }
393         }
394         return counter;
395     }
396 
397     /**
398      * Coordinates of token end. Used to prevent inline ternary
399      * operator from being processed twice.
400      */
401     private static class TokenEnd {
402 
403         /** End line of token. */
404         private int endLineNo;
405 
406         /** End column of token. */
407         private int endColumnNo;
408 
409         /**
410          * Sets end coordinates from given token.
411          * @param endToken token.
412          */
413         public void setToken(DetailAST endToken) {
414             if (!isAfter(endToken)) {
415                 endLineNo = endToken.getLineNo();
416                 endColumnNo = endToken.getColumnNo();
417             }
418         }
419 
420         /** Sets end token coordinates to the start of the file. */
421         public void reset() {
422             endLineNo = 0;
423             endColumnNo = 0;
424         }
425 
426         /**
427          * Checks if saved coordinates located after given token.
428          * @param ast given token.
429          * @return true, if saved coordinates located after given token.
430          */
431         public boolean isAfter(DetailAST ast) {
432             final int lineNo = ast.getLineNo();
433             final int columnNo = ast.getColumnNo();
434             boolean isAfter = true;
435             if (lineNo > endLineNo
436                     || lineNo == endLineNo
437                     && columnNo > endColumnNo) {
438                 isAfter = false;
439             }
440             return isAfter;
441         }
442 
443     }
444 
445     /**
446      * Class that store range value and expression value.
447      */
448     private static class Values {
449 
450         /** NP value for range. */
451         private final BigInteger rangeValue;
452 
453         /** NP value for expression. */
454         private final BigInteger expressionValue;
455 
456         /**
457          * Constructor that assigns all of class fields.
458          * @param valueOfRange NP value for range
459          * @param valueOfExpression NP value for expression
460          */
461         /* package */ Values(BigInteger valueOfRange, BigInteger valueOfExpression) {
462             rangeValue = valueOfRange;
463             expressionValue = valueOfExpression;
464         }
465 
466         /**
467          * Returns NP value for range.
468          * @return NP value for range
469          */
470         public BigInteger getRangeValue() {
471             return rangeValue;
472         }
473 
474         /**
475          * Returns NP value for expression.
476          * @return NP value for expression
477          */
478         public BigInteger getExpressionValue() {
479             return expressionValue;
480         }
481 
482     }
483 
484 }