/** * BSD-style license; for more info see http://pmd.sourceforge.net/license.html */ package net.sourceforge.pmd.lang.java.rule.design; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import net.sourceforge.pmd.RuleContext; import net.sourceforge.pmd.lang.java.ast.ASTAllocationExpression; import net.sourceforge.pmd.lang.java.ast.ASTCatchStatement; import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit; import net.sourceforge.pmd.lang.java.ast.ASTConditionalAndExpression; import net.sourceforge.pmd.lang.java.ast.ASTConditionalExpression; import net.sourceforge.pmd.lang.java.ast.ASTConditionalOrExpression; import net.sourceforge.pmd.lang.java.ast.ASTForStatement; import net.sourceforge.pmd.lang.java.ast.ASTIfStatement; import net.sourceforge.pmd.lang.java.ast.ASTLiteral; import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclarator; import net.sourceforge.pmd.lang.java.ast.ASTName; import net.sourceforge.pmd.lang.java.ast.ASTPrimaryExpression; import net.sourceforge.pmd.lang.java.ast.ASTPrimaryPrefix; import net.sourceforge.pmd.lang.java.ast.ASTPrimarySuffix; import net.sourceforge.pmd.lang.java.ast.ASTSwitchLabel; import net.sourceforge.pmd.lang.java.ast.ASTWhileStatement; import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule; import net.sourceforge.pmd.lang.java.rule.JavaRuleViolation; import net.sourceforge.pmd.lang.java.symboltable.ClassScope; import net.sourceforge.pmd.lang.java.symboltable.SourceFileScope; import net.sourceforge.pmd.lang.java.symboltable.VariableNameDeclaration; import net.sourceforge.pmd.lang.symboltable.Scope; import net.sourceforge.pmd.util.StringUtil; /** * The God Class Rule detects a the God Class design flaw using metrics. A god * class does too many things, is very big and complex. It should be split apart * to be more object-oriented. The rule uses the detection strategy described in * [1]. The violations are reported against the entire class. * * [1] Lanza. Object-Oriented Metrics in Practice. Page 80. * * @since 5.0 */ public class GodClassRule extends AbstractJavaRule { /** * Very high threshold for WMC (Weighted Method Count). See: Lanza. * Object-Oriented Metrics in Practice. Page 16. */ private static final int WMC_VERY_HIGH = 47; /** * Few means between 2 and 5. See: Lanza. Object-Oriented Metrics in * Practice. Page 18. */ private static final int FEW_THRESHOLD = 5; /** * One third is a low value. See: Lanza. Object-Oriented Metrics in * Practice. Page 17. */ private static final double ONE_THIRD_THRESHOLD = 1.0 / 3.0; /** The Weighted Method Count metric. */ private int wmcCounter; /** The Access To Foreign Data metric. */ private int atfdCounter; /** * Collects for each method of the current class, which local attributes are * accessed. */ private Map<String, Set<String>> methodAttributeAccess; /** The name of the current method. */ private String currentMethodName; /** * Base entry point for the visitor - the compilation unit (everything * within one file). The metrics are initialized. Then the other nodes are * visited. Afterwards the metrics are evaluated against fixed thresholds. */ @Override public Object visit(ASTCompilationUnit node, Object data) { wmcCounter = 0; atfdCounter = 0; methodAttributeAccess = new HashMap<>(); Object result = super.visit(node, data); double tcc = calculateTcc(); // StringBuilder debug = new StringBuilder(); // debug.append("Values for class ") // .append(node.getImage()).append(": ") // .append("WMC=").append(wmcCounter).append(", ") // .append("ATFD=").append(atfdCounter).append(", ") // .append("TCC=").append(tcc); // System.out.println(debug.toString()); if (wmcCounter >= WMC_VERY_HIGH && atfdCounter > FEW_THRESHOLD && tcc < ONE_THIRD_THRESHOLD) { StringBuilder sb = new StringBuilder(); sb.append(getMessage()).append(" (").append("WMC=").append(wmcCounter).append(", ").append("ATFD=") .append(atfdCounter).append(", ").append("TCC=").append(tcc).append(')'); RuleContext ctx = (RuleContext) data; ctx.getReport().addRuleViolation(new JavaRuleViolation(this, ctx, node, sb.toString())); } return result; } /** * Calculates the Tight Class Cohesion metric. * * @return a value between 0 and 1. */ private double calculateTcc() { double tcc = 0.0; int methodPairs = determineMethodPairs(); double totalMethodPairs = calculateTotalMethodPairs(); if (totalMethodPairs > 0) { tcc = methodPairs / totalMethodPairs; } return tcc; } /** * Calculates the number of possible method pairs. Its basically the sum of * the first (methodCount - 1) integers. It will be 0, if no methods exist * or only one method, means, if no pairs exist. * * @return */ private double calculateTotalMethodPairs() { int methodCount = methodAttributeAccess.size(); int n = methodCount - 1; double totalMethodPairs = n * (n + 1) / 2.0; return totalMethodPairs; } /** * Uses the {@link #methodAttributeAccess} map to detect method pairs, that * use at least one common attribute of the class. * * @return */ private int determineMethodPairs() { List<String> methods = new ArrayList<>(methodAttributeAccess.keySet()); int methodCount = methods.size(); int pairs = 0; if (methodCount > 1) { for (int i = 0; i < methodCount; i++) { for (int j = i + 1; j < methodCount; j++) { String firstMethodName = methods.get(i); String secondMethodName = methods.get(j); Set<String> accessesOfFirstMethod = methodAttributeAccess.get(firstMethodName); Set<String> accessesOfSecondMethod = methodAttributeAccess.get(secondMethodName); Set<String> combinedAccesses = new HashSet<>(); combinedAccesses.addAll(accessesOfFirstMethod); combinedAccesses.addAll(accessesOfSecondMethod); if (combinedAccesses.size() < (accessesOfFirstMethod.size() + accessesOfSecondMethod.size())) { pairs++; } } } } return pairs; } /** * The primary expression node is used to detect access to attributes and * method calls. If the access is not for a foreign class, then the * {@link #methodAttributeAccess} map is updated for the current method. */ @Override public Object visit(ASTPrimaryExpression node, Object data) { if (isForeignAttributeOrMethod(node)) { if (isAttributeAccess(node) || isMethodCall(node) && isForeignGetterSetterCall(node)) { atfdCounter++; } } else { if (currentMethodName != null) { Set<String> methodAccess = methodAttributeAccess.get(currentMethodName); String variableName = getVariableName(node); VariableNameDeclaration variableDeclaration = findVariableDeclaration(variableName, node.getScope().getEnclosingScope(ClassScope.class)); if (variableDeclaration != null) { methodAccess.add(variableName); } } } return super.visit(node, data); } private boolean isForeignGetterSetterCall(ASTPrimaryExpression node) { String methodOrAttributeName = getMethodOrAttributeName(node); return methodOrAttributeName != null && StringUtil.startsWithAny(methodOrAttributeName, "get", "is", "set"); } private boolean isMethodCall(ASTPrimaryExpression node) { boolean result = false; List<ASTPrimarySuffix> suffixes = node.findDescendantsOfType(ASTPrimarySuffix.class); if (suffixes.size() == 1) { result = suffixes.get(0).isArguments(); } return result; } private boolean isForeignAttributeOrMethod(ASTPrimaryExpression node) { boolean result = false; String nameImage = getNameImage(node); if (nameImage != null && (!nameImage.contains(".") || nameImage.startsWith("this."))) { result = false; } else if (nameImage == null && node.getFirstDescendantOfType(ASTPrimaryPrefix.class).usesThisModifier()) { result = false; } else if (nameImage == null && node.hasDecendantOfAnyType(ASTLiteral.class, ASTAllocationExpression.class)) { result = false; } else { result = true; } return result; } private String getNameImage(ASTPrimaryExpression node) { ASTPrimaryPrefix prefix = node.getFirstDescendantOfType(ASTPrimaryPrefix.class); ASTName name = prefix.getFirstDescendantOfType(ASTName.class); String image = null; if (name != null) { image = name.getImage(); } return image; } private String getVariableName(ASTPrimaryExpression node) { ASTPrimaryPrefix prefix = node.getFirstDescendantOfType(ASTPrimaryPrefix.class); ASTName name = prefix.getFirstDescendantOfType(ASTName.class); String variableName = null; if (name != null) { int dotIndex = name.getImage().indexOf("."); if (dotIndex == -1) { variableName = name.getImage(); } else { variableName = name.getImage().substring(0, dotIndex); } } return variableName; } private String getMethodOrAttributeName(ASTPrimaryExpression node) { ASTPrimaryPrefix prefix = node.getFirstDescendantOfType(ASTPrimaryPrefix.class); ASTName name = prefix.getFirstDescendantOfType(ASTName.class); String methodOrAttributeName = null; if (name != null) { int dotIndex = name.getImage().indexOf("."); if (dotIndex > -1) { methodOrAttributeName = name.getImage().substring(dotIndex + 1); } } return methodOrAttributeName; } private VariableNameDeclaration findVariableDeclaration(String variableName, Scope scope) { VariableNameDeclaration result = null; for (VariableNameDeclaration declaration : scope.getDeclarations(VariableNameDeclaration.class).keySet()) { if (declaration.getImage().equals(variableName)) { result = declaration; break; } } if (result == null && scope.getParent() != null && !(scope.getParent() instanceof SourceFileScope)) { result = findVariableDeclaration(variableName, scope.getParent()); } return result; } private boolean isAttributeAccess(ASTPrimaryExpression node) { return node.findDescendantsOfType(ASTPrimarySuffix.class).isEmpty(); } @Override public Object visit(ASTMethodDeclaration node, Object data) { wmcCounter++; currentMethodName = node.getFirstChildOfType(ASTMethodDeclarator.class).getImage(); methodAttributeAccess.put(currentMethodName, new HashSet<String>()); Object result = super.visit(node, data); currentMethodName = null; return result; } @Override public Object visit(ASTConditionalOrExpression node, Object data) { wmcCounter++; return super.visit(node, data); } @Override public Object visit(ASTConditionalAndExpression node, Object data) { wmcCounter++; return super.visit(node, data); } @Override public Object visit(ASTIfStatement node, Object data) { wmcCounter++; return super.visit(node, data); } @Override public Object visit(ASTWhileStatement node, Object data) { wmcCounter++; return super.visit(node, data); } @Override public Object visit(ASTForStatement node, Object data) { wmcCounter++; return super.visit(node, data); } @Override public Object visit(ASTSwitchLabel node, Object data) { wmcCounter++; return super.visit(node, data); } @Override public Object visit(ASTCatchStatement node, Object data) { wmcCounter++; return super.visit(node, data); } @Override public Object visit(ASTConditionalExpression node, Object data) { if (node.isTernary()) { wmcCounter++; } return super.visit(node, data); } }