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