View Javadoc

1   /**
2    * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3    */
4   package net.sourceforge.pmd.lang.java.rule.coupling;
5   
6   import java.util.ArrayList;
7   import java.util.Collections;
8   import java.util.Iterator;
9   import java.util.List;
10  import java.util.Set;
11  
12  import net.sourceforge.pmd.RuleContext;
13  import net.sourceforge.pmd.lang.java.ast.ASTAllocationExpression;
14  import net.sourceforge.pmd.lang.java.ast.ASTAssignmentOperator;
15  import net.sourceforge.pmd.lang.java.ast.ASTBlock;
16  import net.sourceforge.pmd.lang.java.ast.ASTForStatement;
17  import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration;
18  import net.sourceforge.pmd.lang.java.ast.ASTName;
19  import net.sourceforge.pmd.lang.java.ast.ASTPrimaryExpression;
20  import net.sourceforge.pmd.lang.java.ast.ASTPrimaryPrefix;
21  import net.sourceforge.pmd.lang.java.ast.ASTPrimarySuffix;
22  import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclarator;
23  import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclaratorId;
24  import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
25  import net.sourceforge.pmd.lang.java.symboltable.LocalScope;
26  import net.sourceforge.pmd.lang.java.symboltable.Scope;
27  import net.sourceforge.pmd.lang.java.symboltable.VariableNameDeclaration;
28  
29  /**
30   * This rule can detect possible violations of the Law of Demeter.
31   * The Law of Demeter is a simple rule, that says "only talk to friends". It helps to reduce
32   * coupling between classes or objects.
33   * <p>
34   * See:
35   * <ul>
36   *   <li>Andrew Hunt, David Thomas, and Ward Cunningham. The Pragmatic Programmer. From Journeyman to Master. Addison-Wesley Longman, Amsterdam, October 1999.</li>
37   *   <li>K.J. Lieberherr and I.M. Holland. Assuring good style for object-oriented programs. Software, IEEE, 6(5):38–48, 1989.</li>
38   * </ul>
39   * 
40   * @since 5.0
41   *
42   */
43  public class LawOfDemeterRule extends AbstractJavaRule {
44      private static final String REASON_METHOD_CHAIN_CALLS = "method chain calls";
45      private static final String REASON_OBJECT_NOT_CREATED_LOCALLY = "object not created locally";
46      private static final String REASON_STATIC_ACCESS = "static property access";
47      
48      /**
49       * That's a new method. We are going to check each method call inside the method.
50       * @return <code>null</code>.
51       */
52      @Override
53      public Object visit(ASTMethodDeclaration node, Object data) {
54          List<ASTPrimaryExpression> primaryExpressions = node.findDescendantsOfType(ASTPrimaryExpression.class);
55          for (ASTPrimaryExpression expression : primaryExpressions) {
56              List<MethodCall> calls = MethodCall.createMethodCalls(expression);
57              addViolations(calls, (RuleContext)data);
58          }
59          return null;
60      }
61      
62      private void addViolations(List<MethodCall> calls, RuleContext ctx) {
63          for (MethodCall method : calls) {
64              if (method.isViolation()) {
65                  addViolationWithMessage(ctx, method.getExpression(), getMessage() + " (" + method.getViolationReason() + ")");
66              }
67          }
68      }
69      
70      
71      /**
72       * Collects the information of one identified method call. The method call
73       * might be a violation of the Law of Demeter or not.
74       */
75      private static class MethodCall {
76          private static final String METHOD_CALL_CHAIN = "result from previous method call";
77          private static final String SIMPLE_ASSIGNMENT_OPERATOR = "=";
78          private static final String SCOPE_METHOD_CHAINING = "method-chaining";
79          private static final String SCOPE_CLASS = "class";
80          private static final String SCOPE_METHOD = "method";
81          private static final String SCOPE_LOCAL = "local";
82          private static final String SCOPE_STATIC_CHAIN = "static-chain";
83          private static final String SUPER = "super";
84          private static final String THIS = "this";
85          
86          private ASTPrimaryExpression expression;
87          private String baseName;
88          private String methodName;
89          private String baseScope;
90          private String baseTypeName;
91          private Class<?> baseType;
92          private boolean violation;
93          private String violationReason;
94          
95          /**
96           * Create a new method call for the prefix expression part of the primary expression.
97           */
98          private MethodCall(ASTPrimaryExpression expression, ASTPrimaryPrefix prefix) {
99              this.expression = expression;
100             analyze(prefix);
101             determineType();
102             checkViolation();
103         }
104 
105         /**
106          * Create a new method call for the given suffix expression part of the primary expression.
107          * This is used for method chains.
108          */
109         private MethodCall(ASTPrimaryExpression expression, ASTPrimarySuffix suffix) {
110             this.expression = expression;
111             analyze(suffix);
112             determineType();
113             checkViolation();
114         }
115         
116         /**
117          * Factory method to convert a given primary expression into MethodCalls.
118          * In case the primary expression represents a method chain call, then multiple
119          * MethodCalls are returned.
120          * 
121          * @return a list of MethodCalls, might be empty.
122          */
123         public static List<MethodCall> createMethodCalls(ASTPrimaryExpression expression) {
124             List<MethodCall> result = new ArrayList<MethodCall>();
125 
126             if (isNotAConstructorCall(expression) && hasSuffixesWithArguments(expression)) {
127                 ASTPrimaryPrefix prefixNode = expression.getFirstDescendantOfType(ASTPrimaryPrefix.class);
128                 result.add(new MethodCall(expression, prefixNode));
129                 
130                 List<ASTPrimarySuffix> suffixes = findSuffixesWithoutArguments(expression);
131                 for (ASTPrimarySuffix suffix : suffixes) {
132                     result.add(new MethodCall(expression, suffix));
133                 }
134             }
135             
136             return result;
137         }
138         
139         private static boolean isNotAConstructorCall(ASTPrimaryExpression expression) {
140             return !expression.hasDescendantOfType(ASTAllocationExpression.class);
141         }
142 
143         private static List<ASTPrimarySuffix> findSuffixesWithoutArguments(ASTPrimaryExpression expr) {
144             List<ASTPrimarySuffix> result = new ArrayList<ASTPrimarySuffix>();
145             if (hasRealPrefix(expr)) {
146                 List<ASTPrimarySuffix> suffixes = expr.findDescendantsOfType(ASTPrimarySuffix.class);
147                 for (ASTPrimarySuffix suffix : suffixes) {
148                     if (!suffix.isArguments()) {
149                         result.add(suffix);
150                     }
151                 }
152             }
153             return result;
154         }
155         
156         private static boolean hasRealPrefix(ASTPrimaryExpression expr) {
157             ASTPrimaryPrefix prefix = expr.getFirstDescendantOfType(ASTPrimaryPrefix.class);
158             return !prefix.usesThisModifier() && !prefix.usesSuperModifier();
159         }
160         
161         private static boolean hasSuffixesWithArguments(ASTPrimaryExpression expr) {
162             boolean result = false;
163             if (hasRealPrefix(expr)) {
164                 List<ASTPrimarySuffix> suffixes = expr.findDescendantsOfType(ASTPrimarySuffix.class);
165                 for (ASTPrimarySuffix suffix : suffixes) {
166                     if (suffix.isArguments()) {
167                         result = true;
168                         break;
169                     }
170                 }
171             }
172             return result;
173         }
174 
175         private void analyze(ASTPrimaryPrefix prefixNode) {
176             List<ASTName> names = prefixNode.findDescendantsOfType(ASTName.class);
177             
178             baseName = "unknown";
179             methodName = "unknown";
180             
181             if (!names.isEmpty()) {
182                 baseName = names.get(0).getImage();
183                 
184                 int dot = baseName.lastIndexOf('.');
185                 if (dot == -1) {
186                     methodName = baseName;
187                     baseName = THIS;
188                 } else {
189                     methodName = baseName.substring(dot + 1);
190                     baseName = baseName.substring(0, dot);
191                 }
192                 
193             } else {
194                 if (prefixNode.usesThisModifier()) {
195                     baseName = THIS;
196                 } else if (prefixNode.usesSuperModifier()) {
197                     baseName = SUPER;
198                 }
199             }
200         }
201         
202         private void analyze(ASTPrimarySuffix suffix) {
203             baseName = METHOD_CALL_CHAIN;
204             methodName = suffix.getImage();
205         }
206         
207         private void checkViolation() {
208             violation = false;
209             violationReason = null;
210             
211             if (SCOPE_LOCAL.equals(baseScope)) {
212                 Assignment lastAssignment = determineLastAssignment();
213                 if (lastAssignment != null
214                     && !lastAssignment.allocation
215                     && !lastAssignment.iterator
216                     && !lastAssignment.forLoop) {
217                     violation = true;
218                     violationReason = REASON_OBJECT_NOT_CREATED_LOCALLY;
219                 }
220             } else if (SCOPE_METHOD_CHAINING.equals(baseScope)) {
221                 violation = true;
222                 violationReason = REASON_METHOD_CHAIN_CALLS;
223             } else if (SCOPE_STATIC_CHAIN.equals(baseScope)) {
224                 violation = true;
225                 violationReason = REASON_STATIC_ACCESS;
226             }
227         }
228         
229         private void determineType() {
230             VariableNameDeclaration var = null;
231             Scope scope = expression.getScope();
232             
233             baseScope = SCOPE_LOCAL;
234             var = findInLocalScope(baseName, (LocalScope)scope);
235             if (var == null) {
236                 baseScope = SCOPE_METHOD;
237                 var = determineTypeOfVariable(baseName, scope.getEnclosingMethodScope().getVariableDeclarations().keySet());
238             }
239             if (var == null) {
240                 baseScope = SCOPE_CLASS;
241                 var = determineTypeOfVariable(baseName, scope.getEnclosingClassScope().getVariableDeclarations().keySet());
242             }
243             if (var == null) {
244                 baseScope = SCOPE_METHOD_CHAINING;
245             }
246             if (var == null && (THIS.equals(baseName) || SUPER.equals(baseName))) {
247                 baseScope = SCOPE_CLASS;
248             }
249             
250             if (var != null) {
251                 baseTypeName = var.getTypeImage();
252                 baseType = var.getType();
253             } else if (METHOD_CALL_CHAIN.equals(baseName)) {
254                 baseScope = SCOPE_METHOD_CHAINING;
255             } else if (baseName.contains(".") && !baseName.startsWith("System.")) {
256                 baseScope = SCOPE_STATIC_CHAIN;
257             } else {
258                 // everything else is no violation - probably a static method call.
259                 baseScope = null;
260             }
261         }
262         
263         private VariableNameDeclaration findInLocalScope(String name, LocalScope scope) {
264             VariableNameDeclaration result = null;
265             
266             result = determineTypeOfVariable(name, scope.getVariableDeclarations().keySet());
267             if (result == null && scope.getParent() instanceof LocalScope) {
268                 result = findInLocalScope(name, (LocalScope)scope.getParent());
269             }
270             
271             return result;
272         }
273 
274         private VariableNameDeclaration determineTypeOfVariable(String variableName, Set<VariableNameDeclaration> declarations) {
275             VariableNameDeclaration result = null;
276             for (VariableNameDeclaration var : declarations) {
277                 if (variableName.equals(var.getImage())) {
278                     result = var;
279                     break;
280                 }
281             }
282             return result;
283         }
284         
285         private Assignment determineLastAssignment() {
286             List<Assignment> assignments = new ArrayList<Assignment>();
287             
288             ASTBlock block = expression.getFirstParentOfType(ASTMethodDeclaration.class).getFirstChildOfType(ASTBlock.class);
289             
290             List<ASTVariableDeclarator> variableDeclarators = block.findDescendantsOfType(ASTVariableDeclarator.class);
291             for (ASTVariableDeclarator declarator : variableDeclarators) {
292                 ASTVariableDeclaratorId variableDeclaratorId = declarator.getFirstChildOfType(ASTVariableDeclaratorId.class);
293                 if (variableDeclaratorId.hasImageEqualTo(baseName)) {
294                     boolean allocationFound = declarator.getFirstDescendantOfType(ASTAllocationExpression.class) != null;
295                     boolean iterator = isIterator();
296                     boolean forLoop = isForLoop(declarator);
297                     assignments.add(new Assignment(declarator.getBeginLine(), allocationFound, iterator, forLoop));
298                 }
299             }
300             
301             List<ASTAssignmentOperator> assignmentStmts = block.findDescendantsOfType(ASTAssignmentOperator.class);
302             for (ASTAssignmentOperator stmt : assignmentStmts) {
303                 if (stmt.hasImageEqualTo(SIMPLE_ASSIGNMENT_OPERATOR)) {
304                     boolean allocationFound = stmt.jjtGetParent().getFirstDescendantOfType(ASTAllocationExpression.class) != null;
305                     boolean iterator = isIterator();
306                     assignments.add(new Assignment(stmt.getBeginLine(), allocationFound, iterator, false));
307                 }
308             }
309             
310             Assignment result = null;
311             if (!assignments.isEmpty()) {
312                 Collections.sort(assignments);
313                 result = assignments.get(0);
314             }
315             return result;
316         }
317         
318         private boolean isIterator() {
319             boolean iterator = false;
320             if ((baseType != null && baseType == Iterator.class)
321                     || (baseTypeName != null && baseTypeName.endsWith("Iterator"))) {
322                 iterator = true;
323             }
324             return iterator;
325         }
326         
327         private boolean isForLoop(ASTVariableDeclarator declarator) {
328             return declarator.jjtGetParent().jjtGetParent() instanceof ASTForStatement;
329         }
330 
331         public ASTPrimaryExpression getExpression() {
332             return expression;
333         }
334         
335         public boolean isViolation() {
336             return violation;
337         }
338         
339         public String getViolationReason() {
340             return violationReason;
341         }
342         
343         @Override
344         public String toString() {
345             return "MethodCall on line " + expression.getBeginLine() + ":\n"
346                 + "  " + baseName + " name: "+ methodName+ "\n"
347                 + "  type: " + baseTypeName + " (" + baseType + "), \n"
348                 + "  scope: " + baseScope + "\n"
349                 + "  violation: " + violation + " (" + violationReason + ")\n";
350         }
351         
352     }
353     
354     /**
355      * Stores the assignment of a variable and whether the variable's value is
356      * allocated locally (new constructor call). The class is comparable, so that
357      * the last assignment can be determined.
358      */
359     private static class Assignment implements Comparable<Assignment> {
360         private int line;
361         private boolean allocation;
362         private boolean iterator;
363         private boolean forLoop;
364         
365         public Assignment(int line, boolean allocation, boolean iterator, boolean forLoop) {
366             this.line = line;
367             this.allocation = allocation;
368             this.iterator = iterator;
369             this.forLoop = forLoop;
370         }
371         
372         @Override
373         public String toString() {
374             return "assignment: line=" + line + " allocation:" + allocation
375                 + " iterator:" + iterator + " forLoop: " + forLoop;
376         }
377 
378         public int compareTo(Assignment o) {
379             return o.line - line;
380         }
381     }
382 }