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.design;
5   
6   import java.util.ArrayList;
7   import java.util.HashMap;
8   import java.util.HashSet;
9   import java.util.List;
10  import java.util.Map;
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.ASTCatchStatement;
16  import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceDeclaration;
17  import net.sourceforge.pmd.lang.java.ast.ASTConditionalAndExpression;
18  import net.sourceforge.pmd.lang.java.ast.ASTConditionalExpression;
19  import net.sourceforge.pmd.lang.java.ast.ASTConditionalOrExpression;
20  import net.sourceforge.pmd.lang.java.ast.ASTForStatement;
21  import net.sourceforge.pmd.lang.java.ast.ASTIfStatement;
22  import net.sourceforge.pmd.lang.java.ast.ASTLiteral;
23  import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration;
24  import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclarator;
25  import net.sourceforge.pmd.lang.java.ast.ASTName;
26  import net.sourceforge.pmd.lang.java.ast.ASTPrimaryExpression;
27  import net.sourceforge.pmd.lang.java.ast.ASTPrimaryPrefix;
28  import net.sourceforge.pmd.lang.java.ast.ASTPrimarySuffix;
29  import net.sourceforge.pmd.lang.java.ast.ASTSwitchLabel;
30  import net.sourceforge.pmd.lang.java.ast.ASTWhileStatement;
31  import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
32  import net.sourceforge.pmd.lang.java.rule.JavaRuleViolation;
33  import net.sourceforge.pmd.lang.java.symboltable.Scope;
34  import net.sourceforge.pmd.lang.java.symboltable.SourceFileScope;
35  import net.sourceforge.pmd.lang.java.symboltable.VariableNameDeclaration;
36  import net.sourceforge.pmd.util.StringUtil;
37  
38  /**
39   * The God Class Rule detects a the God Class design flaw using metrics. A god class does too many things,
40   * is very big and complex. It should be split apart to be more object-oriented.
41   * The rule uses the detection strategy described in [1]. The violations are reported
42   * against the entire class.
43   * 
44   * [1] Lanza. Object-Oriented Metrics in Practice. Page 80.
45   * 
46   * @since 5.0
47   */
48  public class GodClassRule extends AbstractJavaRule {
49  
50      /**
51       * Very high threshold for WMC (Weighted Method Count).
52       * See: Lanza. Object-Oriented Metrics in Practice. Page 16.
53       */
54      private static final int WMC_VERY_HIGH = 47;
55      
56      /**
57       * Few means between 2 and 5.
58       * See: Lanza. Object-Oriented Metrics in Practice. Page 18.
59       */
60      private static final int FEW_THRESHOLD = 5;
61      
62      /**
63       * One third is a low value.
64       * See: Lanza. Object-Oriented Metrics in Practice. Page 17.
65       */
66      private static final double ONE_THIRD_THRESHOLD = 1.0/3.0;
67      
68      /** The Weighted Method Count metric. */
69      private int wmcCounter;
70      /** The Access To Foreign Data metric. */
71      private int atfdCounter;
72  
73      /** Collects for each method of the current class, which local attributes are accessed. */
74      private Map<String, Set<String>> methodAttributeAccess;
75      /** The name of the current method. */
76      private String currentMethodName;
77      
78      
79      /**
80       * Base entry point for the visitor - the class declaration.
81       * The metrics are initialized. Then the other nodes are visited. Afterwards
82       * the metrics are evaluated against fixed thresholds.
83       */
84      @Override
85      public Object visit(ASTClassOrInterfaceDeclaration node, Object data) {
86          wmcCounter = 0;
87          atfdCounter = 0;
88          methodAttributeAccess = new HashMap<String, Set<String>>();
89          
90          Object result = super.visit(node, data);
91          
92          double tcc = calculateTcc();
93  
94  //        StringBuilder debug = new StringBuilder();
95  //            debug.append("Values for class ")
96  //            .append(node.getImage()).append(": ")
97  //            .append("WMC=").append(wmcCounter).append(", ")
98  //            .append("ATFD=").append(atfdCounter).append(", ")
99  //            .append("TCC=").append(tcc);
100 //        System.out.println(debug.toString());
101 
102         if (wmcCounter >= WMC_VERY_HIGH
103             && atfdCounter > FEW_THRESHOLD
104             && tcc < ONE_THIRD_THRESHOLD) {
105 
106             StringBuilder sb = new StringBuilder();
107             sb.append(getMessage());
108             sb.append(" (")
109                 .append("WMC=").append(wmcCounter).append(", ")
110                 .append("ATFD=").append(atfdCounter).append(", ")
111                 .append("TCC=").append(tcc).append(')');
112             
113             RuleContext ctx = (RuleContext)data;
114             ctx.getReport().addRuleViolation(new JavaRuleViolation(this, ctx, node, sb.toString()));
115         }
116         return result;
117     }
118 
119     /**
120      * Calculates the Tight Class Cohesion metric.
121      * @return a value between 0 and 1.
122      */
123     private double calculateTcc() {
124         double tcc = 0.0;
125         int methodPairs = determineMethodPairs();
126         double totalMethodPairs = calculateTotalMethodPairs();
127         if (totalMethodPairs > 0) {
128             tcc = methodPairs / totalMethodPairs;
129         }
130         return tcc;
131     }
132 
133     /**
134      * Calculates the number of possible method pairs.
135      * Its basically the sum of the first (methodCount - 1) integers.
136      * It will be 0, if no methods exist or only one method, means, if no pairs exist.
137      * @return
138      */
139     private double calculateTotalMethodPairs() {
140         int methodCount = methodAttributeAccess.size();
141         int n = methodCount - 1;
142         double totalMethodPairs = n * (n + 1) / 2.0;
143         return totalMethodPairs;
144     }
145     
146     /**
147      * Uses the {@link #methodAttributeAccess} map to detect method pairs, that use at least
148      * one common attribute of the class.
149      * @return
150      */
151     private int determineMethodPairs() {
152         List<String> methods = new ArrayList<String>(methodAttributeAccess.keySet());
153         int methodCount = methods.size();
154         int pairs = 0;
155         
156         if (methodCount > 1) {
157             for (int i = 0; i < methodCount - 1; i++) {
158                 String firstMethodName = methods.get(i);
159                 String secondMethodName = methods.get(i + 1);
160                 Set<String> accessesOfFirstMethod = methodAttributeAccess.get(firstMethodName);
161                 Set<String> accessesOfSecondMethod = methodAttributeAccess.get(secondMethodName);
162                 Set<String> combinedAccesses = new HashSet<String>();
163                 
164                 combinedAccesses.addAll(accessesOfFirstMethod);
165                 combinedAccesses.addAll(accessesOfSecondMethod);
166                 
167                 if (combinedAccesses.size() < (accessesOfFirstMethod.size() + accessesOfSecondMethod.size())) {
168                     pairs++;
169                 }
170             }
171         }
172         return pairs;
173     }
174 
175 
176     /**
177      * The primary expression node is used to detect access to attributes and method calls.
178      * If the access is not for a foreign class, then the {@link #methodAttributeAccess} map is
179      * updated for the current method.
180      */
181     @Override
182     public Object visit(ASTPrimaryExpression node, Object data) {
183         if (isForeignAttributeOrMethod(node)) {
184             if (isAttributeAccess(node)
185                 || (isMethodCall(node) && isForeignGetterSetterCall(node))) {
186                 atfdCounter++;
187             }
188         } else {
189             if (currentMethodName != null) {
190                 Set<String> methodAccess = methodAttributeAccess.get(currentMethodName);
191                 String variableName = getVariableName(node);
192                 VariableNameDeclaration variableDeclaration = findVariableDeclaration(variableName, node.getScope().getEnclosingClassScope());
193                 if (variableDeclaration != null) {
194                     methodAccess.add(variableName);
195                 }
196             }
197         }
198         
199         return super.visit(node, data);
200     }
201 
202 
203     private boolean isForeignGetterSetterCall(ASTPrimaryExpression node) {
204 
205         String methodOrAttributeName = getMethodOrAttributeName(node);
206         
207         return methodOrAttributeName != null && StringUtil.startsWithAny(methodOrAttributeName, "get","is","set");
208     }
209 
210 
211     private boolean isMethodCall(ASTPrimaryExpression node) {
212         boolean result = false;
213         List<ASTPrimarySuffix> suffixes = node.findDescendantsOfType(ASTPrimarySuffix.class);
214         if (suffixes.size() == 1) {
215             result = suffixes.get(0).isArguments();
216         }
217         return result;
218     }
219 
220 
221     private boolean isForeignAttributeOrMethod(ASTPrimaryExpression node) {
222         boolean result = false;
223         String nameImage = getNameImage(node);
224         
225         if (nameImage != null && (!nameImage.contains(".") || nameImage.startsWith("this."))) {
226             result = false;
227         } else if (nameImage == null && node.getFirstDescendantOfType(ASTPrimaryPrefix.class).usesThisModifier()) {
228             result = false;
229         } else if (nameImage == null && node.hasDecendantOfAnyType(ASTLiteral.class, ASTAllocationExpression.class)) {
230             result = false;
231         } else {
232             result = true;
233         }
234         
235         return result;
236     }
237     
238     private String getNameImage(ASTPrimaryExpression node) {
239         ASTPrimaryPrefix prefix = node.getFirstDescendantOfType(ASTPrimaryPrefix.class);
240         ASTName name = prefix.getFirstDescendantOfType(ASTName.class);
241 
242         String image = null;
243         if (name != null) {
244             image = name.getImage();
245         }
246         return image;
247     }
248 
249     private String getVariableName(ASTPrimaryExpression node) {
250         ASTPrimaryPrefix prefix = node.getFirstDescendantOfType(ASTPrimaryPrefix.class);
251         ASTName name = prefix.getFirstDescendantOfType(ASTName.class);
252 
253         String variableName = null;
254         
255         if (name != null) {
256             int dotIndex = name.getImage().indexOf(".");
257             if (dotIndex == -1) {
258                 variableName = name.getImage();
259             } else {
260                 variableName = name.getImage().substring(0, dotIndex);
261             }
262         }
263         
264         return variableName;
265     }
266     
267     private String getMethodOrAttributeName(ASTPrimaryExpression node) {
268         ASTPrimaryPrefix prefix = node.getFirstDescendantOfType(ASTPrimaryPrefix.class);
269         ASTName name = prefix.getFirstDescendantOfType(ASTName.class);
270 
271         String methodOrAttributeName = null;
272         
273         if (name != null) {
274             int dotIndex = name.getImage().indexOf(".");
275             if (dotIndex > -1) {
276                 methodOrAttributeName = name.getImage().substring(dotIndex + 1);
277             }
278         }
279         
280         return methodOrAttributeName;
281     }
282 
283     private VariableNameDeclaration findVariableDeclaration(String variableName, Scope scope) {
284         VariableNameDeclaration result = null;
285         
286         for (VariableNameDeclaration declaration : scope.getVariableDeclarations().keySet()) {
287             if (declaration.getImage().equals(variableName)) {
288                 result = declaration;
289                 break;
290             }
291         }
292         
293         if (result == null && scope.getParent() != null && !(scope.getParent() instanceof SourceFileScope)) {
294             result = findVariableDeclaration(variableName, scope.getParent());
295         }
296         
297         return result;
298     }
299 
300     private boolean isAttributeAccess(ASTPrimaryExpression node) {
301         return node.findDescendantsOfType(ASTPrimarySuffix.class).isEmpty();
302     }
303 
304 
305 
306     @Override
307     public Object visit(ASTMethodDeclaration node, Object data) {
308         wmcCounter++;
309         
310         currentMethodName = node.getFirstChildOfType(ASTMethodDeclarator.class).getImage();
311         methodAttributeAccess.put(currentMethodName, new HashSet<String>());
312         
313         Object result = super.visit(node, data);
314         
315         currentMethodName = null;
316         
317         return result;
318     }
319 
320     @Override
321     public Object visit(ASTConditionalOrExpression node, Object data) {
322         wmcCounter++;
323         return super.visit(node, data);
324     }
325 
326     @Override
327     public Object visit(ASTConditionalAndExpression node, Object data) {
328         wmcCounter++;
329         return super.visit(node, data);
330     }
331 
332     @Override
333     public Object visit(ASTIfStatement node, Object data) {
334         wmcCounter++;
335         return super.visit(node, data);
336     }
337 
338     @Override
339     public Object visit(ASTWhileStatement node, Object data) {
340         wmcCounter++;
341         return super.visit(node, data);
342     }
343 
344     @Override
345     public Object visit(ASTForStatement node, Object data) {
346         wmcCounter++;
347         return super.visit(node, data);
348     }
349 
350     @Override
351     public Object visit(ASTSwitchLabel node, Object data) {
352         wmcCounter++;
353         return super.visit(node, data);
354     }
355 
356     @Override
357     public Object visit(ASTCatchStatement node, Object data) {
358         wmcCounter++;
359         return super.visit(node, data);
360     }
361 
362     @Override
363     public Object visit(ASTConditionalExpression node, Object data) {
364         if (node.isTernary()) {
365             wmcCounter++;
366         }
367         return super.visit(node, data);
368     }
369     
370     
371 }