/** * BSD-style license; for more info see http://pmd.sourceforge.net/license.html */ package net.sourceforge.pmd.lang.java.rule.codesize; import java.util.ArrayDeque; import java.util.Deque; import net.sourceforge.pmd.lang.ast.Node; import net.sourceforge.pmd.lang.java.ast.ASTBlockStatement; import net.sourceforge.pmd.lang.java.ast.ASTCatchStatement; import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit; import net.sourceforge.pmd.lang.java.ast.ASTConditionalExpression; import net.sourceforge.pmd.lang.java.ast.ASTConstructorDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTDoStatement; import net.sourceforge.pmd.lang.java.ast.ASTEnumDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTForStatement; import net.sourceforge.pmd.lang.java.ast.ASTIfStatement; import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclarator; import net.sourceforge.pmd.lang.java.ast.ASTSwitchLabel; import net.sourceforge.pmd.lang.java.ast.ASTSwitchStatement; import net.sourceforge.pmd.lang.java.ast.ASTWhileStatement; import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule; import net.sourceforge.pmd.lang.rule.properties.BooleanProperty; import net.sourceforge.pmd.lang.rule.properties.IntegerProperty; /** * Implements the standard cyclomatic complexity rule * <p> * Standard rules: +1 for each decision point, including case statements but not * including boolean operators unlike CyclomaticComplexityRule. * * @author Alan Hohn, based on work by Donald A. Leckie * * @since June 18, 2014 */ public class StdCyclomaticComplexityRule extends AbstractJavaRule { 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; protected 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); } } protected Deque<Entry> entryStack = new ArrayDeque<>(); public StdCyclomaticComplexityRule() { definePropertyDescriptor(REPORT_LEVEL_DESCRIPTOR); definePropertyDescriptor(SHOW_CLASSES_COMPLEXITY_DESCRIPTOR); definePropertyDescriptor(SHOW_METHODS_COMPLEXITY_DESCRIPTOR); } @Override public Object visit(ASTCompilationUnit node, Object data) { reportLevel = getProperty(REPORT_LEVEL_DESCRIPTOR); showClassesComplexity = getProperty(SHOW_CLASSES_COMPLEXITY_DESCRIPTOR); showMethodsComplexity = getProperty(SHOW_METHODS_COMPLEXITY_DESCRIPTOR); super.visit(node, data); return data; } @Override public Object visit(ASTIfStatement node, Object data) { entryStack.peek().bumpDecisionPoints(); super.visit(node, data); return data; } @Override public Object visit(ASTCatchStatement node, Object data) { entryStack.peek().bumpDecisionPoints(); super.visit(node, data); return data; } @Override public Object visit(ASTForStatement node, Object data) { entryStack.peek().bumpDecisionPoints(); super.visit(node, data); return data; } @Override public Object visit(ASTDoStatement node, Object data) { entryStack.peek().bumpDecisionPoints(); super.visit(node, data); return data; } @Override public Object visit(ASTSwitchStatement node, Object data) { Entry entry = entryStack.peek(); int childCount = node.jjtGetNumChildren(); int lastIndex = childCount - 1; for (int n = 0; n < lastIndex; n++) { Node childNode = node.jjtGetChild(n); if (childNode instanceof ASTSwitchLabel) { // default is generally not considered a decision (same as // "else") ASTSwitchLabel sl = (ASTSwitchLabel) childNode; if (!sl.isDefault()) { childNode = node.jjtGetChild(n + 1); if (childNode instanceof ASTBlockStatement) { entry.bumpDecisionPoints(); } } } } super.visit(node, data); return data; } @Override public Object visit(ASTWhileStatement node, Object data) { entryStack.peek().bumpDecisionPoints(); super.visit(node, data); return data; } @Override public Object visit(ASTConditionalExpression node, Object data) { if (node.isTernary()) { entryStack.peek().bumpDecisionPoints(); super.visit(node, data); } return data; } @Override public Object visit(ASTClassOrInterfaceDeclaration node, Object data) { if (node.isInterface()) { return data; } entryStack.push(new Entry(node)); super.visit(node, data); Entry classEntry = entryStack.pop(); if (showClassesComplexity) { if (classEntry.getComplexityAverage() >= reportLevel || classEntry.highestDecisionPoints >= reportLevel) { addViolation(data, node, new String[] { "class", node.getImage(), classEntry.getComplexityAverage() + " (Highest = " + classEntry.highestDecisionPoints + ')', }); } } return data; } @Override public Object visit(ASTMethodDeclaration node, Object data) { entryStack.push(new Entry(node)); super.visit(node, data); Entry methodEntry = entryStack.pop(); if (!isSuppressed(node)) { 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 (showMethodsComplexity && methodEntry.decisionPoints >= reportLevel) { addViolation(data, node, new String[] { "method", methodDeclarator == null ? "" : methodDeclarator.getImage(), String.valueOf(methodEntry.decisionPoints), }); } } return data; } @Override public Object visit(ASTEnumDeclaration node, Object data) { entryStack.push(new Entry(node)); super.visit(node, data); Entry classEntry = entryStack.pop(); if (classEntry.getComplexityAverage() >= reportLevel || classEntry.highestDecisionPoints >= reportLevel) { addViolation(data, node, new String[] { "class", node.getImage(), classEntry.getComplexityAverage() + "(Highest = " + classEntry.highestDecisionPoints + ')', }); } return data; } @Override public Object visit(ASTConstructorDeclaration node, Object data) { entryStack.push(new Entry(node)); super.visit(node, data); Entry constructorEntry = entryStack.pop(); if (!isSuppressed(node)) { int constructorDecisionPointCount = constructorEntry.decisionPoints; Entry classEntry = entryStack.peek(); classEntry.methodCount++; classEntry.decisionPoints += constructorDecisionPointCount; if (constructorDecisionPointCount > classEntry.highestDecisionPoints) { classEntry.highestDecisionPoints = constructorDecisionPointCount; } if (showMethodsComplexity && constructorEntry.decisionPoints >= reportLevel) { addViolation(data, node, new String[] { "constructor", classEntry.node.getImage(), String.valueOf(constructorDecisionPointCount), }); } } return data; } }