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