1
2
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
31
32
33
34
35
36
37
38
39
40
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
50
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
73
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
97
98 private MethodCall(ASTPrimaryExpression expression, ASTPrimaryPrefix prefix) {
99 this.expression = expression;
100 analyze(prefix);
101 determineType();
102 checkViolation();
103 }
104
105
106
107
108
109 private MethodCall(ASTPrimaryExpression expression, ASTPrimarySuffix suffix) {
110 this.expression = expression;
111 analyze(suffix);
112 determineType();
113 checkViolation();
114 }
115
116
117
118
119
120
121
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
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
356
357
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 }