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