/** * BSD-style license; for more info see http://pmd.sourceforge.net/license.html */ package net.sourceforge.pmd.lang.plsql.rule.codesize; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import net.sourceforge.pmd.lang.plsql.ast.ASTCaseStatement; import net.sourceforge.pmd.lang.plsql.ast.ASTCaseWhenClause; import net.sourceforge.pmd.lang.plsql.ast.ASTConditionalAndExpression; import net.sourceforge.pmd.lang.plsql.ast.ASTConditionalOrExpression; import net.sourceforge.pmd.lang.plsql.ast.ASTElseClause; import net.sourceforge.pmd.lang.plsql.ast.ASTElsifClause; import net.sourceforge.pmd.lang.plsql.ast.ASTExpression; import net.sourceforge.pmd.lang.plsql.ast.ASTForStatement; import net.sourceforge.pmd.lang.plsql.ast.ASTIfStatement; import net.sourceforge.pmd.lang.plsql.ast.ASTLoopStatement; import net.sourceforge.pmd.lang.plsql.ast.ASTMethodDeclaration; import net.sourceforge.pmd.lang.plsql.ast.ASTProgramUnit; import net.sourceforge.pmd.lang.plsql.ast.ASTReturnStatement; import net.sourceforge.pmd.lang.plsql.ast.ASTStatement; import net.sourceforge.pmd.lang.plsql.ast.ASTTriggerTimingPointSection; import net.sourceforge.pmd.lang.plsql.ast.ASTTriggerUnit; import net.sourceforge.pmd.lang.plsql.ast.ASTTypeMethod; import net.sourceforge.pmd.lang.plsql.ast.ASTWhileStatement; import net.sourceforge.pmd.lang.plsql.ast.ExecutableCode; import net.sourceforge.pmd.lang.plsql.ast.PLSQLNode; import net.sourceforge.pmd.lang.plsql.rule.AbstractStatisticalPLSQLRule; import net.sourceforge.pmd.stat.DataPoint; import net.sourceforge.pmd.util.NumericConstants; /** * NPath complexity is a measurement of the acyclic execution paths through a * function. See Nejmeh, Communications of the ACM Feb 1988 pp 188-200. * * @author Jason Bennett */ public class NPathComplexityRule extends AbstractStatisticalPLSQLRule { private static final String CLASS_NAME = NPathComplexityRule.class.getCanonicalName(); private static final Logger LOGGER = Logger.getLogger(NPathComplexityRule.class.getName()); public NPathComplexityRule() { super(); setProperty(MINIMUM_DESCRIPTOR, 200d); } private int complexityMultipleOf(PLSQLNode node, int npathStart, Object data) { LOGGER.entering(CLASS_NAME, "complexityMultipleOf(SimpleNode)"); int npath = npathStart; PLSQLNode n; for (int i = 0; i < node.jjtGetNumChildren(); i++) { n = (PLSQLNode) node.jjtGetChild(i); npath *= (Integer) n.jjtAccept(this, data); } LOGGER.exiting(CLASS_NAME, "complexityMultipleOf(SimpleNode)", npath); return npath; } @Override public Object visit(ASTMethodDeclaration node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTMethodDeclaration)"); int npath = complexityMultipleOf(node, 1, data); DataPoint point = new DataPoint(); point.setNode(node); point.setScore(1.0 * npath); point.setMessage(getMessage()); addDataPoint(point); if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest("NPath complexity: " + npath + " for line " + node.getBeginLine() + ", column " + node.getBeginColumn()); } LOGGER.exiting(CLASS_NAME, "visit(ASTMethodDeclaration)", npath); return Integer.valueOf(npath); } @Override public Object visit(ASTProgramUnit node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTProgramUnit)"); int npath = complexityMultipleOf(node, 1, data); DataPoint point = new DataPoint(); point.setNode(node); point.setScore(1.0 * npath); point.setMessage(getMessage()); addDataPoint(point); if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest("NPath complexity: " + npath + " for line " + node.getBeginLine() + ", column " + node.getBeginColumn()); } LOGGER.exiting(CLASS_NAME, "visit(ASTProgramUnit)", npath); return Integer.valueOf(npath); } @Override public Object visit(ASTTypeMethod node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTTypeMethod)"); int npath = complexityMultipleOf(node, 1, data); DataPoint point = new DataPoint(); point.setNode(node); point.setScore(1.0 * npath); point.setMessage(getMessage()); addDataPoint(point); if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest("NPath complexity: " + npath + " for line " + node.getBeginLine() + ", column " + node.getBeginColumn()); } LOGGER.exiting(CLASS_NAME, "visit(ASTTypeMethod)", npath); return Integer.valueOf(npath); } @Override public Object visit(ASTTriggerUnit node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTTriggerUnit)"); int npath = complexityMultipleOf(node, 1, data); DataPoint point = new DataPoint(); point.setNode(node); point.setScore(1.0 * npath); point.setMessage(getMessage()); addDataPoint(point); if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest("NPath complexity: " + npath + " for line " + node.getBeginLine() + ", column " + node.getBeginColumn()); } LOGGER.exiting(CLASS_NAME, "visit(ASTTriggerUnit)", npath); return Integer.valueOf(npath); } @Override public Object visit(ASTTriggerTimingPointSection node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTTriggerTimingPointSection)"); int npath = complexityMultipleOf(node, 1, data); DataPoint point = new DataPoint(); point.setNode(node); point.setScore(1.0 * npath); point.setMessage(getMessage()); addDataPoint(point); if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest("NPath complexity: " + npath + " for line " + node.getBeginLine() + ", column " + node.getBeginColumn()); } LOGGER.exiting(CLASS_NAME, "visit(ASTTriggerTimingPointSection)", npath); return Integer.valueOf(npath); } @Override public Object visit(PLSQLNode node, Object data) { LOGGER.entering(CLASS_NAME, "visit(SimpleNode)"); int npath = complexityMultipleOf(node, 1, data); LOGGER.exiting(CLASS_NAME, "visit(SimpleNode)", npath); return Integer.valueOf(npath); } @Override public Object visit(ASTIfStatement node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTIfStatement)"); // (npath of if + npath of else (or 1) + bool_comp of if) * npath of // next int boolCompIf = sumExpressionComplexity(node.getFirstChildOfType(ASTExpression.class)); int complexity = 0; List<PLSQLNode> statementChildren = new ArrayList<>(); for (int i = 0; i < node.jjtGetNumChildren(); i++) { if (node.jjtGetChild(i).getClass() == ASTStatement.class || node.jjtGetChild(i).getClass() == ASTElsifClause.class || node.jjtGetChild(i).getClass() == ASTElseClause.class) { statementChildren.add((PLSQLNode) node.jjtGetChild(i)); } } if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest(statementChildren.size() + " statementChildren found for IF statement " + node.getBeginLine() + ", column " + node.getBeginColumn()); } /* * SRT if (statementChildren.isEmpty() || statementChildren.size() == 1 * && ( null != node.getFirstChildOfType(ASTElseClause.class) ) * //.hasElse() || statementChildren.size() != 1 && ( null == * node.getFirstChildOfType(ASTElseClause.class) ) // !node.hasElse() ) * { throw new * IllegalStateException("If node has wrong number of children"); } */ /* * @TODO Any explicit Elsif clause(s) and Else clause are included in * the list of statements // add path for not taking if if (null == * node.getFirstChildOfType(ASTElsifClause.class) ) // * !node.hasElse()!node.hasElse()) { complexity++; } * * if (null == node.getFirstChildOfType(ASTElseClause.class) ) // * !node.hasElse()!node.hasElse()) { complexity++; } */ for (PLSQLNode element : statementChildren) { complexity += (Integer) element.jjtAccept(this, data); } LOGGER.exiting(CLASS_NAME, "visit(ASTIfStatement)", boolCompIf + complexity); return Integer.valueOf(boolCompIf + complexity); } @Override public Object visit(ASTElsifClause node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTElsifClause)"); // (npath of if + npath of else (or 1) + bool_comp of if) * npath of // next int boolCompIf = sumExpressionComplexity(node.getFirstChildOfType(ASTExpression.class)); int complexity = 0; List<PLSQLNode> statementChildren = new ArrayList<>(); for (int i = 0; i < node.jjtGetNumChildren(); i++) { if (node.jjtGetChild(i).getClass() == ASTStatement.class) { statementChildren.add((PLSQLNode) node.jjtGetChild(i)); } } if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest(statementChildren.size() + " statementChildren found for ELSIF statement " + node.getBeginLine() + ", column " + node.getBeginColumn()); } /* * SRT if (statementChildren.isEmpty() || statementChildren.size() == 1 * && ( null != node.getFirstChildOfType(ASTElseClause.class) ) * //.hasElse() || statementChildren.size() != 1 && ( null == * node.getFirstChildOfType(ASTElseClause.class) ) // !node.hasElse() ) * { throw new * IllegalStateException("If node has wrong number of children"); } */ for (PLSQLNode element : statementChildren) { complexity += (Integer) element.jjtAccept(this, data); } LOGGER.exiting(CLASS_NAME, "visit(ASTElsifClause)", boolCompIf + complexity); return Integer.valueOf(boolCompIf + complexity); } @Override public Object visit(ASTElseClause node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTElseClause)"); // (npath of if + npath of else (or 1) + bool_comp of if) * npath of // next int complexity = 0; List<PLSQLNode> statementChildren = new ArrayList<>(); for (int i = 0; i < node.jjtGetNumChildren(); i++) { if (node.jjtGetChild(i).getClass() == ASTStatement.class) { statementChildren.add((PLSQLNode) node.jjtGetChild(i)); } } if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest(statementChildren.size() + " statementChildren found for ELSE clause statement " + node.getBeginLine() + ", column " + node.getBeginColumn()); } for (PLSQLNode element : statementChildren) { complexity += (Integer) element.jjtAccept(this, data); } LOGGER.exiting(CLASS_NAME, "visit(ASTElseClause)", complexity); return Integer.valueOf(complexity); } @Override public Object visit(ASTWhileStatement node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTWhileStatement)"); // (npath of while + bool_comp of while + 1) * npath of next int boolCompWhile = sumExpressionComplexity(node.getFirstChildOfType(ASTExpression.class)); Integer nPathWhile = (Integer) ((PLSQLNode) node.getFirstChildOfType(ASTStatement.class)).jjtAccept(this, data); LOGGER.exiting(CLASS_NAME, "visit(ASTWhileStatement)", boolCompWhile + nPathWhile + 1); return Integer.valueOf(boolCompWhile + nPathWhile + 1); } @Override public Object visit(ASTLoopStatement node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTLoopStatement)"); // (npath of do + bool_comp of do + 1) * npath of next int boolCompDo = sumExpressionComplexity(node.getFirstChildOfType(ASTExpression.class)); Integer nPathDo = (Integer) ((PLSQLNode) node.getFirstChildOfType(ASTStatement.class)).jjtAccept(this, data); LOGGER.exiting(CLASS_NAME, "visit(ASTLoopStatement)", boolCompDo + nPathDo + 1); return Integer.valueOf(boolCompDo + nPathDo + 1); } @Override public Object visit(ASTForStatement node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTForStatement)"); // (npath of for + bool_comp of for + 1) * npath of next int boolCompFor = sumExpressionComplexity(node.getFirstDescendantOfType(ASTExpression.class)); Integer nPathFor = (Integer) ((PLSQLNode) node.getFirstChildOfType(ASTStatement.class)).jjtAccept(this, data); LOGGER.exiting(CLASS_NAME, "visit(ASTForStatement)", boolCompFor + nPathFor + 1); return Integer.valueOf(boolCompFor + nPathFor + 1); } @Override public Object visit(ASTReturnStatement node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTReturnStatement)"); // return statements are valued at 1, or the value of the boolean // expression ASTExpression expr = node.getFirstChildOfType(ASTExpression.class); if (expr == null) { return NumericConstants.ONE; } int boolCompReturn = sumExpressionComplexity(expr); int conditionalExpressionComplexity = complexityMultipleOf(expr, 1, data); if (conditionalExpressionComplexity > 1) { boolCompReturn += conditionalExpressionComplexity; } if (boolCompReturn > 0) { return Integer.valueOf(boolCompReturn); } LOGGER.entering(CLASS_NAME, "visit(ASTReturnStatement)", NumericConstants.ONE); return NumericConstants.ONE; } @Override public Object visit(ASTCaseWhenClause node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTCaseWhenClause)"); // bool_comp of switch + sum(npath(case_range)) int boolCompSwitch = sumExpressionComplexity(node.getFirstChildOfType(ASTExpression.class)); int npath = 1; int caseRange = 0; for (int i = 0; i < node.jjtGetNumChildren(); i++) { PLSQLNode n = (PLSQLNode) node.jjtGetChild(i); // Fall-through labels count as 1 for complexity Integer complexity = (Integer) n.jjtAccept(this, data); caseRange *= complexity; } // add in npath of last label npath += caseRange; LOGGER.exiting(CLASS_NAME, "visit(ASTCaseWhenClause)", (boolCompSwitch + npath)); return Integer.valueOf(boolCompSwitch + npath); } @Override public Object visit(ASTCaseStatement node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTCaseStatement)"); // bool_comp of switch + sum(npath(case_range)) int boolCompSwitch = sumExpressionComplexity(node.getFirstChildOfType(ASTExpression.class)); int npath = 0; int caseRange = 0; for (int i = 0; i < node.jjtGetNumChildren(); i++) { PLSQLNode n = (PLSQLNode) node.jjtGetChild(i); // Fall-through labels count as 1 for complexity Integer complexity = (Integer) n.jjtAccept(this, data); caseRange *= complexity; } // add in npath of last label npath += caseRange; LOGGER.exiting(CLASS_NAME, "visit(ASTCaseStatement)", (boolCompSwitch + npath)); return Integer.valueOf(boolCompSwitch + npath); } @Override public Object visit(ASTConditionalOrExpression node, Object data) { return NumericConstants.ONE; } /** * Calculate the boolean complexity of the given expression. NPath boolean * complexity is the sum of && and || tokens. This is calculated by summing * the number of children of the &&'s (minus one) and the children of the * ||'s (minus one). * * <p>Note that this calculation applies to Cyclomatic Complexity as well.</p> * * @param expr * control structure expression * @return complexity of the boolean expression */ public static int sumExpressionComplexity(ASTExpression expr) { LOGGER.entering(CLASS_NAME, "visit(ASTExpression)"); if (expr == null) { LOGGER.exiting(CLASS_NAME, "visit(ASTExpression)", 0); return 0; } List<ASTConditionalAndExpression> andNodes = expr.findDescendantsOfType(ASTConditionalAndExpression.class); List<ASTConditionalOrExpression> orNodes = expr.findDescendantsOfType(ASTConditionalOrExpression.class); int children = 0; for (ASTConditionalOrExpression element : orNodes) { children += element.jjtGetNumChildren(); children--; } for (ASTConditionalAndExpression element : andNodes) { children += element.jjtGetNumChildren(); children--; } LOGGER.exiting(CLASS_NAME, "visit(ASTExpression)", children); return children; } @Override public Object[] getViolationParameters(DataPoint point) { return new String[] { ((ExecutableCode) point.getNode()).getMethodName(), String.valueOf((int) point.getScore()), }; } }