/** * BSD-style license; for more info see http://pmd.sourceforge.net/license.html */ package net.sourceforge.pmd.lang.plsql.rule.codesize; import java.util.Stack; import java.util.logging.Level; import java.util.logging.Logger; import net.sourceforge.pmd.lang.ast.Node; import net.sourceforge.pmd.lang.plsql.ast.ASTCaseStatement; import net.sourceforge.pmd.lang.plsql.ast.ASTCaseWhenClause; import net.sourceforge.pmd.lang.plsql.ast.ASTConditionalOrExpression; import net.sourceforge.pmd.lang.plsql.ast.ASTElsifClause; import net.sourceforge.pmd.lang.plsql.ast.ASTExceptionHandler; 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.ASTInput; import net.sourceforge.pmd.lang.plsql.ast.ASTLoopStatement; import net.sourceforge.pmd.lang.plsql.ast.ASTMethodDeclarator; import net.sourceforge.pmd.lang.plsql.ast.ASTPackageBody; import net.sourceforge.pmd.lang.plsql.ast.ASTPackageSpecification; import net.sourceforge.pmd.lang.plsql.ast.ASTProgramUnit; 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.ASTTypeSpecification; import net.sourceforge.pmd.lang.plsql.ast.ASTWhileStatement; import net.sourceforge.pmd.lang.plsql.rule.AbstractPLSQLRule; import net.sourceforge.pmd.lang.rule.properties.BooleanProperty; import net.sourceforge.pmd.lang.rule.properties.IntegerProperty; /** * @author Donald A. Leckie, * * @version $Revision: 5956 $, $Date: 2008-04-04 04:59:25 -0500 (Fri, 04 Apr * 2008) $ * @since January 14, 2003 */ public class CyclomaticComplexityRule extends AbstractPLSQLRule { private static final Logger LOGGER = Logger.getLogger(CyclomaticComplexityRule.class.getName()); private static final String CLASS_NAME = CyclomaticComplexityRule.class.getName(); public static final IntegerProperty REPORT_LEVEL_DESCRIPTOR = new IntegerProperty("reportLevel", "Cyclomatic Complexity reporting threshold", 1, 30, 10, 1.0f); public static final BooleanProperty SHOW_CLASSES_COMPLEXITY_DESCRIPTOR = new BooleanProperty( "showClassesComplexity", "Add class average violations to the report", true, 2.0f); public static final BooleanProperty SHOW_METHODS_COMPLEXITY_DESCRIPTOR = new BooleanProperty( "showMethodsComplexity", "Add method average violations to the report", true, 3.0f); private int reportLevel; private boolean showClassesComplexity = true; private boolean showMethodsComplexity = true; private static class Entry { private Node node; private int decisionPoints = 1; public int highestDecisionPoints; public int methodCount; private Entry(Node node) { this.node = node; } public void bumpDecisionPoints() { decisionPoints++; } public void bumpDecisionPoints(int size) { decisionPoints += size; } public int getComplexityAverage() { return (double) methodCount == 0 ? 1 : (int) Math.rint((double) decisionPoints / (double) methodCount); } } private Stack<Entry> entryStack = new Stack<>(); public CyclomaticComplexityRule() { definePropertyDescriptor(REPORT_LEVEL_DESCRIPTOR); definePropertyDescriptor(SHOW_CLASSES_COMPLEXITY_DESCRIPTOR); definePropertyDescriptor(SHOW_METHODS_COMPLEXITY_DESCRIPTOR); } @Override public Object visit(ASTInput node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTInput)"); reportLevel = getProperty(REPORT_LEVEL_DESCRIPTOR); showClassesComplexity = getProperty(SHOW_CLASSES_COMPLEXITY_DESCRIPTOR); showMethodsComplexity = getProperty(SHOW_METHODS_COMPLEXITY_DESCRIPTOR); super.visit(node, data); LOGGER.exiting(CLASS_NAME, "visit(ASTInput)"); return data; } @Override public Object visit(ASTElsifClause node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTElsifClause)"); int boolCompIf = NPathComplexityRule.sumExpressionComplexity(node.getFirstChildOfType(ASTExpression.class)); // If statement always has a complexity of at least 1 boolCompIf++; entryStack.peek().bumpDecisionPoints(boolCompIf); super.visit(node, data); LOGGER.exiting(CLASS_NAME, "visit(ASTElsifClause)"); return data; } @Override public Object visit(ASTIfStatement node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTIfClause)"); int boolCompIf = NPathComplexityRule.sumExpressionComplexity(node.getFirstChildOfType(ASTExpression.class)); // If statement always has a complexity of at least 1 boolCompIf++; entryStack.peek().bumpDecisionPoints(boolCompIf); LOGGER.exiting(CLASS_NAME, "visit(ASTIfClause)"); super.visit(node, data); return data; } @Override public Object visit(ASTExceptionHandler node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTExceptionHandler)"); entryStack.peek().bumpDecisionPoints(); LOGGER.exiting(CLASS_NAME, "visit(ASTExceptionHandler)"); super.visit(node, data); return data; } @Override public Object visit(ASTForStatement node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTForStatement)"); int boolCompFor = NPathComplexityRule .sumExpressionComplexity(node.getFirstDescendantOfType(ASTExpression.class)); // For statement always has a complexity of at least 1 boolCompFor++; entryStack.peek().bumpDecisionPoints(boolCompFor); super.visit(node, data); LOGGER.exiting(CLASS_NAME, "visit(ASTForStatement)"); return data; } @Override public Object visit(ASTLoopStatement node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTLoopStatement)"); int boolCompDo = NPathComplexityRule.sumExpressionComplexity(node.getFirstChildOfType(ASTExpression.class)); // Do statement always has a complexity of at least 1 boolCompDo++; entryStack.peek().bumpDecisionPoints(boolCompDo); super.visit(node, data); LOGGER.exiting(CLASS_NAME, "visit(ASTLoopStatement)"); return data; } @Override public Object visit(ASTCaseStatement node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTCaseStatement)"); Entry entry = entryStack.peek(); int boolCompSwitch = NPathComplexityRule.sumExpressionComplexity(node.getFirstChildOfType(ASTExpression.class)); entry.bumpDecisionPoints(boolCompSwitch); super.visit(node, data); LOGGER.exiting(CLASS_NAME, "visit(ASTCaseStatement)"); return data; } @Override public Object visit(ASTCaseWhenClause node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTCaseWhenClause)"); Entry entry = entryStack.peek(); entry.bumpDecisionPoints(); super.visit(node, data); LOGGER.exiting(CLASS_NAME, "visit(ASTCaseWhenClause)"); return data; } @Override public Object visit(ASTWhileStatement node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTWhileStatement)"); int boolCompWhile = NPathComplexityRule.sumExpressionComplexity(node.getFirstChildOfType(ASTExpression.class)); // While statement always has a complexity of at least 1 boolCompWhile++; entryStack.peek().bumpDecisionPoints(boolCompWhile); super.visit(node, data); LOGGER.exiting(CLASS_NAME, "visit(ASTWhileStatement)"); return data; } @Override public Object visit(ASTConditionalOrExpression node, Object data) { return data; } @Override public Object visit(ASTPackageSpecification node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTPackageSpecification)"); // Treat Package Specification like an Interface LOGGER.exiting(CLASS_NAME, "visit(ASTPackageSpecification)"); return data; } @Override public Object visit(ASTTypeSpecification node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTTypeSpecification)"); // Treat Type Specification like an Interface LOGGER.exiting(CLASS_NAME, "visit(ASTTypeSpecification)"); return data; } @Override public Object visit(ASTPackageBody node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTPackageBody)"); entryStack.push(new Entry(node)); super.visit(node, data); Entry classEntry = entryStack.pop(); if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest("ASTPackageBody: ComplexityAverage==" + classEntry.getComplexityAverage() + ", highestDecisionPoint=" + classEntry.highestDecisionPoints); } if (showClassesComplexity) { if (classEntry.getComplexityAverage() >= reportLevel || classEntry.highestDecisionPoints >= reportLevel) { addViolation(data, node, new String[] { "class", node.getImage(), classEntry.getComplexityAverage() + " (Highest = " + classEntry.highestDecisionPoints + ')', }); } } LOGGER.exiting(CLASS_NAME, "visit(ASTPackageBody)"); return data; } @Override public Object visit(ASTTriggerUnit node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTTriggerUnit)"); entryStack.push(new Entry(node)); super.visit(node, data); Entry classEntry = entryStack.pop(); if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest("ASTTriggerUnit: ComplexityAverage==" + classEntry.getComplexityAverage() + ", highestDecisionPoint=" + classEntry.highestDecisionPoints); } if (showClassesComplexity) { if (classEntry.getComplexityAverage() >= reportLevel || classEntry.highestDecisionPoints >= reportLevel) { addViolation(data, node, new String[] { "class", node.getImage(), classEntry.getComplexityAverage() + " (Highest = " + classEntry.highestDecisionPoints + ')', }); } } LOGGER.exiting(CLASS_NAME, "visit(ASTTriggerUnit)"); return data; } @Override public Object visit(ASTProgramUnit node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTProgramUnit)"); entryStack.push(new Entry(node)); super.visit(node, data); Entry methodEntry = entryStack.pop(); if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest("ASTProgramUnit: ComplexityAverage==" + methodEntry.getComplexityAverage() + ", highestDecisionPoint=" + methodEntry.highestDecisionPoints); } if (showMethodsComplexity) { // Entry methodEntry = entryStack.pop(); int methodDecisionPoints = methodEntry.decisionPoints; // PackageBody (including Object Type Body) if (null != node.getFirstParentOfType(ASTPackageBody.class) // Trigger of any form || null != node.getFirstParentOfType(ASTTriggerUnit.class) // TODO || null != node.getFirstParentOfType(ASTProgramUnit.class) // //Another Procedure // TODO || null != node.getFirstParentOfType(ASTTypeMethod.class) // //Another Type method ) { /* * TODO This does not cope with nested methods We need the * outer most ASTPackageBody ASTTriggerUni ASTProgramUnit * ASTTypeMethod * */ Entry classEntry = entryStack.peek(); classEntry.methodCount++; classEntry.bumpDecisionPoints(methodDecisionPoints); if (methodDecisionPoints > classEntry.highestDecisionPoints) { classEntry.highestDecisionPoints = methodDecisionPoints; } } ASTMethodDeclarator methodDeclarator = null; for (int n = 0; n < node.jjtGetNumChildren(); n++) { Node childNode = node.jjtGetChild(n); if (childNode instanceof ASTMethodDeclarator) { methodDeclarator = (ASTMethodDeclarator) childNode; break; } } if (methodEntry.decisionPoints >= reportLevel) { addViolation(data, node, new String[] { "method", methodDeclarator == null ? "" : methodDeclarator.getImage(), String.valueOf(methodEntry.decisionPoints), }); } } LOGGER.exiting(CLASS_NAME, "visit(ASTProgramUnit)"); return data; } @Override public Object visit(ASTTypeMethod node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTTypeMethod)"); entryStack.push(new Entry(node)); super.visit(node, data); Entry methodEntry = entryStack.pop(); if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest("ASTProgramUnit: ComplexityAverage==" + methodEntry.getComplexityAverage() + ", highestDecisionPoint=" + methodEntry.highestDecisionPoints); } if (showMethodsComplexity) { // Entry methodEntry = entryStack.pop(); int methodDecisionPoints = methodEntry.decisionPoints; // PAckageBody (including Object Type Body) if (null != node.getFirstParentOfType(ASTPackageBody.class)) { /* * TODO This does not cope with nested methods We need the * outer most ASTPackageBody */ Entry classEntry = entryStack.peek(); classEntry.methodCount++; classEntry.bumpDecisionPoints(methodDecisionPoints); if (methodDecisionPoints > classEntry.highestDecisionPoints) { classEntry.highestDecisionPoints = methodDecisionPoints; } } ASTMethodDeclarator methodDeclarator = null; for (int n = 0; n < node.jjtGetNumChildren(); n++) { Node childNode = node.jjtGetChild(n); if (childNode instanceof ASTMethodDeclarator) { methodDeclarator = (ASTMethodDeclarator) childNode; break; } } if (methodEntry.decisionPoints >= reportLevel) { addViolation(data, node, new String[] { "method", methodDeclarator == null ? "" : methodDeclarator.getImage(), String.valueOf(methodEntry.decisionPoints), }); } } LOGGER.exiting(CLASS_NAME, "visit(ASTTypeMethod)"); return data; } @Override public Object visit(ASTTriggerTimingPointSection node, Object data) { LOGGER.entering(CLASS_NAME, "visit(ASTTriggerTimingPointSection)"); entryStack.push(new Entry(node)); super.visit(node, data); Entry methodEntry = entryStack.pop(); if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("ASTTriggerTimingPointSection: ComplexityAverage==" + methodEntry.getComplexityAverage() + ", highestDecisionPoint=" + methodEntry.highestDecisionPoints); } if (showMethodsComplexity) { int methodDecisionPoints = methodEntry.decisionPoints; Entry classEntry = entryStack.peek(); classEntry.methodCount++; classEntry.bumpDecisionPoints(methodDecisionPoints); if (methodDecisionPoints > classEntry.highestDecisionPoints) { classEntry.highestDecisionPoints = methodDecisionPoints; } ASTMethodDeclarator methodDeclarator = null; for (int n = 0; n < node.jjtGetNumChildren(); n++) { Node childNode = node.jjtGetChild(n); if (childNode instanceof ASTMethodDeclarator) { methodDeclarator = (ASTMethodDeclarator) childNode; break; } } if (methodEntry.decisionPoints >= reportLevel) { addViolation(data, node, new String[] { "method", methodDeclarator == null ? "" : methodDeclarator.getImage(), String.valueOf(methodEntry.decisionPoints), }); } } LOGGER.exiting(CLASS_NAME, "visit(ASTTriggerTimingPointSection)"); return data; } }