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