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