//////////////////////////////////////////////////////////////////////////////// // checkstyle: Checks Java source code for adherence to a set of rules. // Copyright (C) 2001-2017 the original author or authors. // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA //////////////////////////////////////////////////////////////////////////////// package com.puppycrawl.tools.checkstyle.checks.metrics; import java.math.BigInteger; import java.util.ArrayDeque; import java.util.Deque; import com.puppycrawl.tools.checkstyle.api.AbstractCheck; import com.puppycrawl.tools.checkstyle.api.DetailAST; import com.puppycrawl.tools.checkstyle.api.TokenTypes; /** * Checks the npath complexity against a specified limit (default = 200). * The npath metric computes the number of possible execution paths * through a function. Similar to the cyclomatic complexity but also * takes into account the nesting of conditional statements and * multi-part boolean expressions. * * @author <a href="mailto:simon@redhillconsulting.com.au">Simon Harris</a> * @author o_sukhodolsky */ // -@cs[AbbreviationAsWordInName] Can't change check name public final class NPathComplexityCheck extends AbstractCheck { /** * A key is pointing to the warning message text in "messages.properties" * file. */ public static final String MSG_KEY = "npathComplexity"; /** Default allowed complexity. */ private static final int DEFAULT_MAX = 200; /** The initial current value. */ private static final BigInteger INITIAL_VALUE = BigInteger.ZERO; /** * Stack of NP values for ranges. */ private final Deque<BigInteger> rangeValues = new ArrayDeque<>(); /** Stack of NP values for expressions. */ private final Deque<Integer> expressionValues = new ArrayDeque<>(); /** Stack of belongs to range values for question operator. */ private final Deque<Boolean> isAfterValues = new ArrayDeque<>(); /** * Range of the last processed expression. Used for checking that ternary operation * which is a part of expression won't be processed for second time. */ private final TokenEnd processingTokenEnd = new TokenEnd(); /** NP value for current range. */ private BigInteger currentRangeValue = INITIAL_VALUE; /** Threshold to report error for. */ private int max = DEFAULT_MAX; /** True, when branch is visited, but not leaved. */ private boolean branchVisited; /** * Set the maximum threshold allowed. * @param max the maximum threshold */ public void setMax(int max) { this.max = max; } @Override public int[] getDefaultTokens() { return getAcceptableTokens(); } @Override public int[] getAcceptableTokens() { return new int[] { TokenTypes.CTOR_DEF, TokenTypes.METHOD_DEF, TokenTypes.STATIC_INIT, TokenTypes.INSTANCE_INIT, TokenTypes.LITERAL_WHILE, TokenTypes.LITERAL_DO, TokenTypes.LITERAL_FOR, TokenTypes.LITERAL_IF, TokenTypes.LITERAL_ELSE, TokenTypes.LITERAL_SWITCH, TokenTypes.CASE_GROUP, TokenTypes.LITERAL_TRY, TokenTypes.LITERAL_CATCH, TokenTypes.QUESTION, TokenTypes.LITERAL_RETURN, TokenTypes.LITERAL_DEFAULT, }; } @Override public int[] getRequiredTokens() { return getAcceptableTokens(); } @Override public void beginTree(DetailAST rootAST) { rangeValues.clear(); expressionValues.clear(); isAfterValues.clear(); processingTokenEnd.reset(); currentRangeValue = INITIAL_VALUE; branchVisited = false; } @Override public void visitToken(DetailAST ast) { switch (ast.getType()) { case TokenTypes.LITERAL_IF: case TokenTypes.LITERAL_SWITCH: case TokenTypes.LITERAL_WHILE: case TokenTypes.LITERAL_DO: case TokenTypes.LITERAL_FOR: visitConditional(ast, 1); break; case TokenTypes.QUESTION: visitUnitaryOperator(ast, 2); break; case TokenTypes.LITERAL_RETURN: visitUnitaryOperator(ast, 0); break; case TokenTypes.CASE_GROUP: final int caseNumber = countCaseTokens(ast); branchVisited = true; pushValue(caseNumber); break; case TokenTypes.LITERAL_ELSE: branchVisited = true; if (currentRangeValue.equals(BigInteger.ZERO)) { currentRangeValue = BigInteger.ONE; } pushValue(0); break; case TokenTypes.LITERAL_TRY: case TokenTypes.LITERAL_CATCH: case TokenTypes.LITERAL_DEFAULT: pushValue(1); break; case TokenTypes.CTOR_DEF: case TokenTypes.METHOD_DEF: case TokenTypes.INSTANCE_INIT: case TokenTypes.STATIC_INIT: pushValue(0); break; default: break; } } @Override public void leaveToken(DetailAST ast) { switch (ast.getType()) { case TokenTypes.LITERAL_WHILE: case TokenTypes.LITERAL_DO: case TokenTypes.LITERAL_FOR: case TokenTypes.LITERAL_IF: case TokenTypes.LITERAL_SWITCH: leaveConditional(); break; case TokenTypes.LITERAL_TRY: leaveMultiplyingConditional(); break; case TokenTypes.LITERAL_RETURN: case TokenTypes.QUESTION: leaveUnitaryOperator(); break; case TokenTypes.LITERAL_CATCH: leaveAddingConditional(); break; case TokenTypes.LITERAL_DEFAULT: leaveBranch(); break; case TokenTypes.LITERAL_ELSE: case TokenTypes.CASE_GROUP: leaveBranch(); branchVisited = false; break; case TokenTypes.CTOR_DEF: case TokenTypes.METHOD_DEF: case TokenTypes.INSTANCE_INIT: case TokenTypes.STATIC_INIT: leaveMethodDef(ast); break; default: break; } } /** * Visits if, while, do-while, for and switch tokens - all of them have expression in * parentheses which is used for calculation. * @param ast visited token. * @param basicBranchingFactor default number of branches added. */ private void visitConditional(DetailAST ast, int basicBranchingFactor) { int expressionValue = basicBranchingFactor; DetailAST bracketed; for (bracketed = ast.findFirstToken(TokenTypes.LPAREN).getNextSibling(); bracketed.getType() != TokenTypes.RPAREN; bracketed = bracketed.getNextSibling()) { expressionValue += countConditionalOperators(bracketed); } processingTokenEnd.setToken(bracketed); pushValue(expressionValue); } /** * Visits ternary operator (?:) and return tokens. They differ from those processed by * visitConditional method in that their expression isn't bracketed. * @param ast visited token. * @param basicBranchingFactor number of branches inherently added by this token. */ private void visitUnitaryOperator(DetailAST ast, int basicBranchingFactor) { final boolean isAfter = processingTokenEnd.isAfter(ast); isAfterValues.push(isAfter); if (!isAfter) { processingTokenEnd.setToken(getLastToken(ast)); final int expressionValue = basicBranchingFactor + countConditionalOperators(ast); pushValue(expressionValue); } } /** * Leaves ternary operator (?:) and return tokens. */ private void leaveUnitaryOperator() { if (!isAfterValues.pop()) { final Values valuePair = popValue(); BigInteger basicRangeValue = valuePair.getRangeValue(); BigInteger expressionValue = valuePair.getExpressionValue(); if (expressionValue.equals(BigInteger.ZERO)) { expressionValue = BigInteger.ONE; } if (basicRangeValue.equals(BigInteger.ZERO)) { basicRangeValue = BigInteger.ONE; } currentRangeValue = currentRangeValue.add(expressionValue).multiply(basicRangeValue); } } /** Leaves while, do, for, if, ternary (?::), return or switch. */ private void leaveConditional() { final Values valuePair = popValue(); final BigInteger expressionValue = valuePair.getExpressionValue(); BigInteger basicRangeValue = valuePair.getRangeValue(); if (currentRangeValue.equals(BigInteger.ZERO)) { currentRangeValue = BigInteger.ONE; } if (basicRangeValue.equals(BigInteger.ZERO)) { basicRangeValue = BigInteger.ONE; } currentRangeValue = currentRangeValue.add(expressionValue).multiply(basicRangeValue); } /** Leaves else, default or case group tokens. */ private void leaveBranch() { final Values valuePair = popValue(); final BigInteger basicRangeValue = valuePair.getRangeValue(); final BigInteger expressionValue = valuePair.getExpressionValue(); if (branchVisited && currentRangeValue.equals(BigInteger.ZERO)) { currentRangeValue = BigInteger.ONE; } currentRangeValue = currentRangeValue.subtract(BigInteger.ONE) .add(basicRangeValue) .add(expressionValue); } /** * Process the end of a method definition. * @param ast the token type representing the method definition */ private void leaveMethodDef(DetailAST ast) { final BigInteger bigIntegerMax = BigInteger.valueOf(max); if (currentRangeValue.compareTo(bigIntegerMax) > 0) { log(ast, MSG_KEY, currentRangeValue, bigIntegerMax); } popValue(); currentRangeValue = INITIAL_VALUE; } /** Leaves catch. */ private void leaveAddingConditional() { currentRangeValue = currentRangeValue.add(popValue().getRangeValue().add(BigInteger.ONE)); } /** * Pushes the current range value on the range value stack. Pushes this token expression value * on the expression value stack. * @param expressionValue value of expression calculated for current token. */ private void pushValue(Integer expressionValue) { rangeValues.push(currentRangeValue); expressionValues.push(expressionValue); currentRangeValue = INITIAL_VALUE; } /** * Pops values from both stack of expression values and stack of range values. * @return pair of head values from both of the stacks. */ private Values popValue() { final int expressionValue = expressionValues.pop(); return new Values(rangeValues.pop(), BigInteger.valueOf(expressionValue)); } /** Leaves try. */ private void leaveMultiplyingConditional() { currentRangeValue = currentRangeValue.add(BigInteger.ONE) .multiply(popValue().getRangeValue().add(BigInteger.ONE)); } /** * Calculates number of conditional operators, including inline ternary operatior, for a token. * @param ast inspected token. * @return number of conditional operators. * @see <a href="http://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.23"> * Java Language Specification, §15.23</a> * @see <a href="http://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.24"> * Java Language Specification, §15.24</a> * @see <a href="http://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.25"> * Java Language Specification, §15.25</a> */ private static int countConditionalOperators(DetailAST ast) { int number = 0; for (DetailAST child = ast.getFirstChild(); child != null; child = child.getNextSibling()) { final int type = child.getType(); if (type == TokenTypes.LOR || type == TokenTypes.LAND) { number++; } else if (type == TokenTypes.QUESTION) { number += 2; } number += countConditionalOperators(child); } return number; } /** * Finds a leaf, which is the most distant from the root. * @param ast the root of tree. * @return the leaf. */ private static DetailAST getLastToken(DetailAST ast) { final DetailAST lastChild = ast.getLastChild(); final DetailAST result; if (lastChild.getFirstChild() == null) { result = lastChild; } else { result = getLastToken(lastChild); } return result; } /** * Counts number of case tokens subject to a case group token. * @param ast case group token. * @return number of case tokens. */ private static int countCaseTokens(DetailAST ast) { int counter = 0; for (DetailAST iterator = ast.getFirstChild(); iterator != null; iterator = iterator.getNextSibling()) { if (iterator.getType() == TokenTypes.LITERAL_CASE) { counter++; } } return counter; } /** * Coordinates of token end. Used to prevent inline ternary * operator from being processed twice. */ private static class TokenEnd { /** End line of token. */ private int endLineNo; /** End column of token. */ private int endColumnNo; /** * Sets end coordinates from given token. * @param endToken token. */ public void setToken(DetailAST endToken) { if (!isAfter(endToken)) { endLineNo = endToken.getLineNo(); endColumnNo = endToken.getColumnNo(); } } /** Sets end token coordinates to the start of the file. */ public void reset() { endLineNo = 0; endColumnNo = 0; } /** * Checks if saved coordinates located after given token. * @param ast given token. * @return true, if saved coordinates located after given token. */ public boolean isAfter(DetailAST ast) { final int lineNo = ast.getLineNo(); final int columnNo = ast.getColumnNo(); boolean isAfter = true; if (lineNo > endLineNo || lineNo == endLineNo && columnNo > endColumnNo) { isAfter = false; } return isAfter; } } /** * Class that store range value and expression value. */ private static class Values { /** NP value for range. */ private final BigInteger rangeValue; /** NP value for expression. */ private final BigInteger expressionValue; /** * Constructor that assigns all of class fields. * @param valueOfRange NP value for range * @param valueOfExpression NP value for expression */ Values(BigInteger valueOfRange, BigInteger valueOfExpression) { rangeValue = valueOfRange; expressionValue = valueOfExpression; } /** * Returns NP value for range. * @return NP value for range */ public BigInteger getRangeValue() { return rangeValue; } /** * Returns NP value for expression. * @return NP value for expression */ public BigInteger getExpressionValue() { return expressionValue; } } }