/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.lang.java.rule.coupling;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import net.sourceforge.pmd.RuleContext;
import net.sourceforge.pmd.lang.java.ast.ASTAllocationExpression;
import net.sourceforge.pmd.lang.java.ast.ASTAssignmentOperator;
import net.sourceforge.pmd.lang.java.ast.ASTBlock;
import net.sourceforge.pmd.lang.java.ast.ASTForStatement;
import net.sourceforge.pmd.lang.java.ast.ASTLiteral;
import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration;
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.ASTVariableDeclarator;
import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclaratorId;
import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
import net.sourceforge.pmd.lang.java.symboltable.ClassScope;
import net.sourceforge.pmd.lang.java.symboltable.LocalScope;
import net.sourceforge.pmd.lang.java.symboltable.MethodScope;
import net.sourceforge.pmd.lang.java.symboltable.TypedNameDeclaration;
import net.sourceforge.pmd.lang.java.symboltable.VariableNameDeclaration;
import net.sourceforge.pmd.lang.symboltable.NameDeclaration;
import net.sourceforge.pmd.lang.symboltable.Scope;
/**
* This rule can detect possible violations of the Law of Demeter. The Law of
* Demeter is a simple rule, that says "only talk to friends". It helps to
* reduce coupling between classes or objects.
* <p>
* See:
* <ul>
* <li>Andrew Hunt, David Thomas, and Ward Cunningham. The Pragmatic Programmer.
* From Journeyman to Master. Addison-Wesley Longman, Amsterdam, October
* 1999.</li>
* <li>K.J. Lieberherr and I.M. Holland. Assuring good style for object-oriented
* programs. Software, IEEE, 6(5):38–48, 1989.</li>
* </ul>
*
* @since 5.0
*
*/
public class LawOfDemeterRule extends AbstractJavaRule {
private static final String REASON_METHOD_CHAIN_CALLS = "method chain calls";
private static final String REASON_OBJECT_NOT_CREATED_LOCALLY = "object not created locally";
private static final String REASON_STATIC_ACCESS = "static property access";
/**
* That's a new method. We are going to check each method call inside the
* method.
*
* @return <code>null</code>.
*/
@Override
public Object visit(ASTMethodDeclaration node, Object data) {
List<ASTPrimaryExpression> primaryExpressions = node.findDescendantsOfType(ASTPrimaryExpression.class);
for (ASTPrimaryExpression expression : primaryExpressions) {
List<MethodCall> calls = MethodCall.createMethodCalls(expression);
addViolations(calls, (RuleContext) data);
}
return null;
}
private void addViolations(List<MethodCall> calls, RuleContext ctx) {
for (MethodCall method : calls) {
if (method.isViolation()) {
addViolationWithMessage(ctx, method.getExpression(),
getMessage() + " (" + method.getViolationReason() + ")");
}
}
}
/**
* Collects the information of one identified method call. The method call
* might be a violation of the Law of Demeter or not.
*/
private static class MethodCall {
private static final String METHOD_CALL_CHAIN = "result from previous method call";
private static final String SIMPLE_ASSIGNMENT_OPERATOR = "=";
private static final String SCOPE_METHOD_CHAINING = "method-chaining";
private static final String SCOPE_CLASS = "class";
private static final String SCOPE_METHOD = "method";
private static final String SCOPE_LOCAL = "local";
private static final String SCOPE_STATIC_CHAIN = "static-chain";
private static final String SUPER = "super";
private static final String THIS = "this";
private ASTPrimaryExpression expression;
private String baseName;
private String methodName;
private String baseScope;
private String baseTypeName;
private Class<?> baseType;
private boolean violation;
private String violationReason;
/**
* Create a new method call for the prefix expression part of the
* primary expression.
*/
private MethodCall(ASTPrimaryExpression expression, ASTPrimaryPrefix prefix) {
this.expression = expression;
analyze(prefix);
determineType();
checkViolation();
}
/**
* Create a new method call for the given suffix expression part of the
* primary expression. This is used for method chains.
*/
private MethodCall(ASTPrimaryExpression expression, ASTPrimarySuffix suffix) {
this.expression = expression;
analyze(suffix);
determineType();
checkViolation();
}
/**
* Factory method to convert a given primary expression into
* MethodCalls. In case the primary expression represents a method chain
* call, then multiple MethodCalls are returned.
*
* @return a list of MethodCalls, might be empty.
*/
public static List<MethodCall> createMethodCalls(ASTPrimaryExpression expression) {
List<MethodCall> result = new ArrayList<>();
if (isNotAConstructorCall(expression) && isNotLiteral(expression) && hasSuffixesWithArguments(expression)) {
ASTPrimaryPrefix prefixNode = expression.getFirstDescendantOfType(ASTPrimaryPrefix.class);
MethodCall firstMethodCallInChain = new MethodCall(expression, prefixNode);
result.add(firstMethodCallInChain);
if (firstMethodCallInChain.isNotBuilder()) {
List<ASTPrimarySuffix> suffixes = findSuffixesWithoutArguments(expression);
for (ASTPrimarySuffix suffix : suffixes) {
result.add(new MethodCall(expression, suffix));
}
}
}
return result;
}
private static boolean isNotAConstructorCall(ASTPrimaryExpression expression) {
return !expression.hasDescendantOfType(ASTAllocationExpression.class);
}
private static boolean isNotLiteral(ASTPrimaryExpression expression) {
ASTPrimaryPrefix prefix = expression.getFirstDescendantOfType(ASTPrimaryPrefix.class);
if (prefix != null) {
return !prefix.hasDescendantOfType(ASTLiteral.class);
}
return true;
}
private boolean isNotBuilder() {
return baseType != StringBuffer.class && baseType != StringBuilder.class
&& !"StringBuilder".equals(baseTypeName) && !"StringBuffer".equals(baseTypeName);
}
private static List<ASTPrimarySuffix> findSuffixesWithoutArguments(ASTPrimaryExpression expr) {
List<ASTPrimarySuffix> result = new ArrayList<>();
if (hasRealPrefix(expr)) {
List<ASTPrimarySuffix> suffixes = expr.findDescendantsOfType(ASTPrimarySuffix.class);
for (ASTPrimarySuffix suffix : suffixes) {
if (!suffix.isArguments()) {
result.add(suffix);
}
}
}
return result;
}
private static boolean hasRealPrefix(ASTPrimaryExpression expr) {
ASTPrimaryPrefix prefix = expr.getFirstDescendantOfType(ASTPrimaryPrefix.class);
return !prefix.usesThisModifier() && !prefix.usesSuperModifier();
}
private static boolean hasSuffixesWithArguments(ASTPrimaryExpression expr) {
boolean result = false;
if (hasRealPrefix(expr)) {
List<ASTPrimarySuffix> suffixes = expr.findDescendantsOfType(ASTPrimarySuffix.class);
for (ASTPrimarySuffix suffix : suffixes) {
if (suffix.isArguments()) {
result = true;
break;
}
}
}
return result;
}
private void analyze(ASTPrimaryPrefix prefixNode) {
List<ASTName> names = prefixNode.findDescendantsOfType(ASTName.class);
baseName = "unknown";
methodName = "unknown";
if (!names.isEmpty()) {
baseName = names.get(0).getImage();
int dot = baseName.lastIndexOf('.');
if (dot == -1) {
methodName = baseName;
baseName = THIS;
} else {
methodName = baseName.substring(dot + 1);
baseName = baseName.substring(0, dot);
}
} else {
if (prefixNode.usesThisModifier()) {
baseName = THIS;
} else if (prefixNode.usesSuperModifier()) {
baseName = SUPER;
}
}
}
private void analyze(ASTPrimarySuffix suffix) {
baseName = METHOD_CALL_CHAIN;
methodName = suffix.getImage();
}
private void checkViolation() {
violation = false;
violationReason = null;
if (SCOPE_LOCAL.equals(baseScope)) {
Assignment lastAssignment = determineLastAssignment();
if (lastAssignment != null && !lastAssignment.allocation && !lastAssignment.iterator
&& !lastAssignment.forLoop) {
violation = true;
violationReason = REASON_OBJECT_NOT_CREATED_LOCALLY;
}
} else if (SCOPE_METHOD_CHAINING.equals(baseScope)) {
violation = true;
violationReason = REASON_METHOD_CHAIN_CALLS;
} else if (SCOPE_STATIC_CHAIN.equals(baseScope)) {
violation = true;
violationReason = REASON_STATIC_ACCESS;
}
}
private void determineType() {
NameDeclaration var = null;
Scope scope = expression.getScope();
baseScope = SCOPE_LOCAL;
var = findInLocalScope(baseName, scope);
if (var == null) {
baseScope = SCOPE_METHOD;
var = determineTypeOfVariable(baseName,
scope.getEnclosingScope(MethodScope.class).getVariableDeclarations().keySet());
}
if (var == null) {
baseScope = SCOPE_CLASS;
var = determineTypeOfVariable(baseName,
scope.getEnclosingScope(ClassScope.class).getVariableDeclarations().keySet());
}
if (var == null) {
baseScope = SCOPE_METHOD_CHAINING;
}
if (var == null && (THIS.equals(baseName) || SUPER.equals(baseName))) {
baseScope = SCOPE_CLASS;
}
if (var instanceof TypedNameDeclaration) {
baseTypeName = ((TypedNameDeclaration) var).getTypeImage();
baseType = ((TypedNameDeclaration) var).getType();
} else if (METHOD_CALL_CHAIN.equals(baseName)) {
baseScope = SCOPE_METHOD_CHAINING;
} else if (baseName.contains(".") && !baseName.startsWith("System.")) {
baseScope = SCOPE_STATIC_CHAIN;
} else {
// everything else is no violation - probably a static method
// call.
baseScope = null;
}
}
private VariableNameDeclaration findInLocalScope(String name, Scope scope) {
VariableNameDeclaration result = null;
result = determineTypeOfVariable(name, scope.getDeclarations(VariableNameDeclaration.class).keySet());
if (result == null && scope.getParent() instanceof LocalScope) {
result = findInLocalScope(name, scope.getParent());
}
return result;
}
private VariableNameDeclaration determineTypeOfVariable(String variableName,
Set<VariableNameDeclaration> declarations) {
VariableNameDeclaration result = null;
for (VariableNameDeclaration var : declarations) {
if (variableName.equals(var.getImage())) {
result = var;
break;
}
}
return result;
}
private Assignment determineLastAssignment() {
List<Assignment> assignments = new ArrayList<>();
ASTBlock block = expression.getFirstParentOfType(ASTMethodDeclaration.class)
.getFirstChildOfType(ASTBlock.class);
//get all variableDeclarators within this block
List<ASTVariableDeclarator> variableDeclarators = block.findDescendantsOfType(ASTVariableDeclarator.class);
for (ASTVariableDeclarator declarator : variableDeclarators) {
ASTVariableDeclaratorId variableDeclaratorId = declarator
.getFirstChildOfType(ASTVariableDeclaratorId.class);
//we only care about it if the image name matches the current baseName
if (variableDeclaratorId.hasImageEqualTo(baseName)) {
boolean allocationFound = declarator
.getFirstDescendantOfType(ASTAllocationExpression.class) != null;
boolean iterator = isIterator() || isFactory(declarator);
boolean forLoop = isForLoop(declarator);
assignments.add(new Assignment(declarator.getBeginLine(), allocationFound, iterator, forLoop));
}
}
//get all AssignmentOperators within this block
List<ASTAssignmentOperator> assignmentStmts = block.findDescendantsOfType(ASTAssignmentOperator.class);
for (ASTAssignmentOperator stmt : assignmentStmts) {
//we only care about it if it occurs prior to (or on) the beginLine of the current expression
//and if it is a simple_assignement_operator
if (stmt.getBeginLine() <= expression.getBeginLine()
&& stmt.hasImageEqualTo(SIMPLE_ASSIGNMENT_OPERATOR)) {
//now we need to make sure it has the right image name
ASTPrimaryPrefix primaryPrefix = stmt.jjtGetParent()
.getFirstDescendantOfType(ASTPrimaryPrefix.class);
if (primaryPrefix != null) {
ASTName prefixName = primaryPrefix.getFirstChildOfType(ASTName.class);
if (prefixName != null && prefixName.hasImageEqualTo(baseName)) {
//this is an assignment related to the baseName we are working with
boolean allocationFound = stmt.jjtGetParent()
.getFirstDescendantOfType(ASTAllocationExpression.class) != null;
boolean iterator = isIterator();
assignments
.add(new Assignment(stmt.getBeginLine(), allocationFound, iterator, false));
}
}
}
}
Assignment result = null;
if (!assignments.isEmpty()) {
//sort them in reverse order and return the first one
Collections.sort(assignments);
result = assignments.get(0);
}
return result;
}
private boolean isIterator() {
boolean iterator = false;
if (baseType != null && baseType == Iterator.class
|| baseTypeName != null && baseTypeName.endsWith("Iterator")) {
iterator = true;
}
return iterator;
}
private boolean isFactory(ASTVariableDeclarator declarator) {
boolean factory = false;
List<ASTName> names = declarator.findDescendantsOfType(ASTName.class);
for (ASTName name : names) {
if (name.getImage().toLowerCase().contains("factory")) {
factory = true;
break;
}
}
return factory;
}
private boolean isForLoop(ASTVariableDeclarator declarator) {
return declarator.jjtGetParent().jjtGetParent() instanceof ASTForStatement;
}
public ASTPrimaryExpression getExpression() {
return expression;
}
public boolean isViolation() {
return violation;
}
public String getViolationReason() {
return violationReason;
}
@Override
public String toString() {
return "MethodCall on line " + expression.getBeginLine() + ":\n" + " " + baseName + " name: " + methodName
+ "\n" + " type: " + baseTypeName + " (" + baseType + "), \n" + " scope: " + baseScope + "\n"
+ " violation: " + violation + " (" + violationReason + ")\n";
}
}
/**
* Stores the assignment of a variable and whether the variable's value is
* allocated locally (new constructor call). The class is comparable, so
* that the last assignment can be determined.
*/
private static class Assignment implements Comparable<Assignment> {
private int line;
private boolean allocation;
private boolean iterator;
private boolean forLoop;
Assignment(int line, boolean allocation, boolean iterator, boolean forLoop) {
this.line = line;
this.allocation = allocation;
this.iterator = iterator;
this.forLoop = forLoop;
}
@Override
public String toString() {
return "assignment: line=" + line + " allocation:" + allocation + " iterator:" + iterator + " forLoop: "
+ forLoop;
}
public int compareTo(Assignment o) {
return o.line - line;
}
}
}