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.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 compilation unit (everything within one file).
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(ASTCompilationUnit 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; i++) {
158                 for (int j = i + 1; j < methodCount; j++) {
159                     String firstMethodName = methods.get(i);
160                     String secondMethodName = methods.get(j);
161                     Set<String> accessesOfFirstMethod = methodAttributeAccess.get(firstMethodName);
162                     Set<String> accessesOfSecondMethod = methodAttributeAccess.get(secondMethodName);
163                     Set<String> combinedAccesses = new HashSet<String>();
164 
165                     combinedAccesses.addAll(accessesOfFirstMethod);
166                     combinedAccesses.addAll(accessesOfSecondMethod);
167 
168                     if (combinedAccesses.size() < (accessesOfFirstMethod.size() + accessesOfSecondMethod.size())) {
169                         pairs++;
170                     }
171                 }
172             }
173         }
174         return pairs;
175     }
176 
177 
178     /**
179      * The primary expression node is used to detect access to attributes and method calls.
180      * If the access is not for a foreign class, then the {@link #methodAttributeAccess} map is
181      * updated for the current method.
182      */
183     @Override
184     public Object visit(ASTPrimaryExpression node, Object data) {
185         if (isForeignAttributeOrMethod(node)) {
186             if (isAttributeAccess(node)
187                 || (isMethodCall(node) && isForeignGetterSetterCall(node))) {
188                 atfdCounter++;
189             }
190         } else {
191             if (currentMethodName != null) {
192                 Set<String> methodAccess = methodAttributeAccess.get(currentMethodName);
193                 String variableName = getVariableName(node);
194                 VariableNameDeclaration variableDeclaration = findVariableDeclaration(variableName, node.getScope().getEnclosingClassScope());
195                 if (variableDeclaration != null) {
196                     methodAccess.add(variableName);
197                 }
198             }
199         }
200         
201         return super.visit(node, data);
202     }
203 
204 
205     private boolean isForeignGetterSetterCall(ASTPrimaryExpression node) {
206 
207         String methodOrAttributeName = getMethodOrAttributeName(node);
208         
209         return methodOrAttributeName != null && StringUtil.startsWithAny(methodOrAttributeName, "get","is","set");
210     }
211 
212 
213     private boolean isMethodCall(ASTPrimaryExpression node) {
214         boolean result = false;
215         List<ASTPrimarySuffix> suffixes = node.findDescendantsOfType(ASTPrimarySuffix.class);
216         if (suffixes.size() == 1) {
217             result = suffixes.get(0).isArguments();
218         }
219         return result;
220     }
221 
222 
223     private boolean isForeignAttributeOrMethod(ASTPrimaryExpression node) {
224         boolean result = false;
225         String nameImage = getNameImage(node);
226         
227         if (nameImage != null && (!nameImage.contains(".") || nameImage.startsWith("this."))) {
228             result = false;
229         } else if (nameImage == null && node.getFirstDescendantOfType(ASTPrimaryPrefix.class).usesThisModifier()) {
230             result = false;
231         } else if (nameImage == null && node.hasDecendantOfAnyType(ASTLiteral.class, ASTAllocationExpression.class)) {
232             result = false;
233         } else {
234             result = true;
235         }
236         
237         return result;
238     }
239     
240     private String getNameImage(ASTPrimaryExpression node) {
241         ASTPrimaryPrefix prefix = node.getFirstDescendantOfType(ASTPrimaryPrefix.class);
242         ASTName name = prefix.getFirstDescendantOfType(ASTName.class);
243 
244         String image = null;
245         if (name != null) {
246             image = name.getImage();
247         }
248         return image;
249     }
250 
251     private String getVariableName(ASTPrimaryExpression node) {
252         ASTPrimaryPrefix prefix = node.getFirstDescendantOfType(ASTPrimaryPrefix.class);
253         ASTName name = prefix.getFirstDescendantOfType(ASTName.class);
254 
255         String variableName = null;
256         
257         if (name != null) {
258             int dotIndex = name.getImage().indexOf(".");
259             if (dotIndex == -1) {
260                 variableName = name.getImage();
261             } else {
262                 variableName = name.getImage().substring(0, dotIndex);
263             }
264         }
265         
266         return variableName;
267     }
268     
269     private String getMethodOrAttributeName(ASTPrimaryExpression node) {
270         ASTPrimaryPrefix prefix = node.getFirstDescendantOfType(ASTPrimaryPrefix.class);
271         ASTName name = prefix.getFirstDescendantOfType(ASTName.class);
272 
273         String methodOrAttributeName = null;
274         
275         if (name != null) {
276             int dotIndex = name.getImage().indexOf(".");
277             if (dotIndex > -1) {
278                 methodOrAttributeName = name.getImage().substring(dotIndex + 1);
279             }
280         }
281         
282         return methodOrAttributeName;
283     }
284 
285     private VariableNameDeclaration findVariableDeclaration(String variableName, Scope scope) {
286         VariableNameDeclaration result = null;
287         
288         for (VariableNameDeclaration declaration : scope.getVariableDeclarations().keySet()) {
289             if (declaration.getImage().equals(variableName)) {
290                 result = declaration;
291                 break;
292             }
293         }
294         
295         if (result == null && scope.getParent() != null && !(scope.getParent() instanceof SourceFileScope)) {
296             result = findVariableDeclaration(variableName, scope.getParent());
297         }
298         
299         return result;
300     }
301 
302     private boolean isAttributeAccess(ASTPrimaryExpression node) {
303         return node.findDescendantsOfType(ASTPrimarySuffix.class).isEmpty();
304     }
305 
306 
307 
308     @Override
309     public Object visit(ASTMethodDeclaration node, Object data) {
310         wmcCounter++;
311         
312         currentMethodName = node.getFirstChildOfType(ASTMethodDeclarator.class).getImage();
313         methodAttributeAccess.put(currentMethodName, new HashSet<String>());
314         
315         Object result = super.visit(node, data);
316         
317         currentMethodName = null;
318         
319         return result;
320     }
321 
322     @Override
323     public Object visit(ASTConditionalOrExpression node, Object data) {
324         wmcCounter++;
325         return super.visit(node, data);
326     }
327 
328     @Override
329     public Object visit(ASTConditionalAndExpression node, Object data) {
330         wmcCounter++;
331         return super.visit(node, data);
332     }
333 
334     @Override
335     public Object visit(ASTIfStatement node, Object data) {
336         wmcCounter++;
337         return super.visit(node, data);
338     }
339 
340     @Override
341     public Object visit(ASTWhileStatement node, Object data) {
342         wmcCounter++;
343         return super.visit(node, data);
344     }
345 
346     @Override
347     public Object visit(ASTForStatement node, Object data) {
348         wmcCounter++;
349         return super.visit(node, data);
350     }
351 
352     @Override
353     public Object visit(ASTSwitchLabel node, Object data) {
354         wmcCounter++;
355         return super.visit(node, data);
356     }
357 
358     @Override
359     public Object visit(ASTCatchStatement node, Object data) {
360         wmcCounter++;
361         return super.visit(node, data);
362     }
363 
364     @Override
365     public Object visit(ASTConditionalExpression node, Object data) {
366         if (node.isTernary()) {
367             wmcCounter++;
368         }
369         return super.visit(node, data);
370     }
371     
372     
373 }