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
41   * class does too many things, is very big and complex. It should be split apart
42   * to be more object-oriented. The rule uses the detection strategy described in
43   * [1]. The violations are reported 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). See: Lanza.
53       * 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. See: Lanza. Object-Oriented Metrics in
59       * Practice. Page 18.
60       */
61      private static final int FEW_THRESHOLD = 5;
62  
63      /**
64       * One third is a low value. See: Lanza. Object-Oriented Metrics in
65       * 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      /**
75       * Collects for each method of the current class, which local attributes are
76       * accessed.
77       */
78      private Map<String, Set<String>> methodAttributeAccess;
79      /** The name of the current method. */
80      private String currentMethodName;
81  
82      /**
83       * Base entry point for the visitor - the compilation unit (everything
84       * within one file). The metrics are initialized. Then the other nodes are
85       * visited. Afterwards the metrics are evaluated against fixed thresholds.
86       */
87      @Override
88      public Object visit(ASTCompilationUnit node, Object data) {
89          wmcCounter = 0;
90          atfdCounter = 0;
91          methodAttributeAccess = new HashMap<>();
92  
93          Object result = super.visit(node, data);
94  
95          double tcc = calculateTcc();
96  
97          // StringBuilder debug = new StringBuilder();
98          // debug.append("Values for class ")
99          // .append(node.getImage()).append(": ")
100         // .append("WMC=").append(wmcCounter).append(", ")
101         // .append("ATFD=").append(atfdCounter).append(", ")
102         // .append("TCC=").append(tcc);
103         // System.out.println(debug.toString());
104 
105         if (wmcCounter >= WMC_VERY_HIGH && atfdCounter > FEW_THRESHOLD && tcc < ONE_THIRD_THRESHOLD) {
106 
107             StringBuilder sb = new StringBuilder();
108             sb.append(getMessage()).append(" (").append("WMC=").append(wmcCounter).append(", ").append("ATFD=")
109                     .append(atfdCounter).append(", ").append("TCC=").append(tcc).append(')');
110 
111             RuleContext ctx = (RuleContext) data;
112             ctx.getReport().addRuleViolation(new JavaRuleViolation(this, ctx, node, sb.toString()));
113         }
114         return result;
115     }
116 
117     /**
118      * Calculates the Tight Class Cohesion metric.
119      * 
120      * @return a value between 0 and 1.
121      */
122     private double calculateTcc() {
123         double tcc = 0.0;
124         int methodPairs = determineMethodPairs();
125         double totalMethodPairs = calculateTotalMethodPairs();
126         if (totalMethodPairs > 0) {
127             tcc = methodPairs / totalMethodPairs;
128         }
129         return tcc;
130     }
131 
132     /**
133      * Calculates the number of possible method pairs. Its basically the sum of
134      * the first (methodCount - 1) integers. It will be 0, if no methods exist
135      * or only one method, means, if no pairs exist.
136      * 
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
148      * use at least one common attribute of the class.
149      * 
150      * @return
151      */
152     private int determineMethodPairs() {
153         List<String> methods = new ArrayList<>(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<>();
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      * The primary expression node is used to detect access to attributes and
180      * method calls. If the access is not for a foreign class, then the
181      * {@link #methodAttributeAccess} map is updated for the current method.
182      */
183     @Override
184     public Object visit(ASTPrimaryExpression node, Object data) {
185         if (isForeignAttributeOrMethod(node)) {
186             if (isAttributeAccess(node) || isMethodCall(node) && isForeignGetterSetterCall(node)) {
187                 atfdCounter++;
188             }
189         } else {
190             if (currentMethodName != null) {
191                 Set<String> methodAccess = methodAttributeAccess.get(currentMethodName);
192                 String variableName = getVariableName(node);
193                 VariableNameDeclaration variableDeclaration = findVariableDeclaration(variableName, node.getScope()
194                         .getEnclosingScope(ClassScope.class));
195                 if (variableDeclaration != null) {
196                     methodAccess.add(variableName);
197                 }
198             }
199         }
200 
201         return super.visit(node, data);
202     }
203 
204     private boolean isForeignGetterSetterCall(ASTPrimaryExpression node) {
205 
206         String methodOrAttributeName = getMethodOrAttributeName(node);
207 
208         return methodOrAttributeName != null && StringUtil.startsWithAny(methodOrAttributeName, "get", "is", "set");
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     private boolean isForeignAttributeOrMethod(ASTPrimaryExpression node) {
221         boolean result = false;
222         String nameImage = getNameImage(node);
223 
224         if (nameImage != null && (!nameImage.contains(".") || nameImage.startsWith("this."))) {
225             result = false;
226         } else if (nameImage == null && node.getFirstDescendantOfType(ASTPrimaryPrefix.class).usesThisModifier()) {
227             result = false;
228         } else if (nameImage == null && node.hasDecendantOfAnyType(ASTLiteral.class, ASTAllocationExpression.class)) {
229             result = false;
230         } else {
231             result = true;
232         }
233 
234         return result;
235     }
236 
237     private String getNameImage(ASTPrimaryExpression node) {
238         ASTPrimaryPrefix prefix = node.getFirstDescendantOfType(ASTPrimaryPrefix.class);
239         ASTName name = prefix.getFirstDescendantOfType(ASTName.class);
240 
241         String image = null;
242         if (name != null) {
243             image = name.getImage();
244         }
245         return image;
246     }
247 
248     private String getVariableName(ASTPrimaryExpression node) {
249         ASTPrimaryPrefix prefix = node.getFirstDescendantOfType(ASTPrimaryPrefix.class);
250         ASTName name = prefix.getFirstDescendantOfType(ASTName.class);
251 
252         String variableName = null;
253 
254         if (name != null) {
255             int dotIndex = name.getImage().indexOf(".");
256             if (dotIndex == -1) {
257                 variableName = name.getImage();
258             } else {
259                 variableName = name.getImage().substring(0, dotIndex);
260             }
261         }
262 
263         return variableName;
264     }
265 
266     private String getMethodOrAttributeName(ASTPrimaryExpression node) {
267         ASTPrimaryPrefix prefix = node.getFirstDescendantOfType(ASTPrimaryPrefix.class);
268         ASTName name = prefix.getFirstDescendantOfType(ASTName.class);
269 
270         String methodOrAttributeName = null;
271 
272         if (name != null) {
273             int dotIndex = name.getImage().indexOf(".");
274             if (dotIndex > -1) {
275                 methodOrAttributeName = name.getImage().substring(dotIndex + 1);
276             }
277         }
278 
279         return methodOrAttributeName;
280     }
281 
282     private VariableNameDeclaration findVariableDeclaration(String variableName, Scope scope) {
283         VariableNameDeclaration result = null;
284 
285         for (VariableNameDeclaration declaration : scope.getDeclarations(VariableNameDeclaration.class).keySet()) {
286             if (declaration.getImage().equals(variableName)) {
287                 result = declaration;
288                 break;
289             }
290         }
291 
292         if (result == null && scope.getParent() != null && !(scope.getParent() instanceof SourceFileScope)) {
293             result = findVariableDeclaration(variableName, scope.getParent());
294         }
295 
296         return result;
297     }
298 
299     private boolean isAttributeAccess(ASTPrimaryExpression node) {
300         return node.findDescendantsOfType(ASTPrimarySuffix.class).isEmpty();
301     }
302 
303     @Override
304     public Object visit(ASTMethodDeclaration node, Object data) {
305         wmcCounter++;
306 
307         currentMethodName = node.getFirstChildOfType(ASTMethodDeclarator.class).getImage();
308         methodAttributeAccess.put(currentMethodName, new HashSet<String>());
309 
310         Object result = super.visit(node, data);
311 
312         currentMethodName = null;
313 
314         return result;
315     }
316 
317     @Override
318     public Object visit(ASTConditionalOrExpression node, Object data) {
319         wmcCounter++;
320         return super.visit(node, data);
321     }
322 
323     @Override
324     public Object visit(ASTConditionalAndExpression node, Object data) {
325         wmcCounter++;
326         return super.visit(node, data);
327     }
328 
329     @Override
330     public Object visit(ASTIfStatement node, Object data) {
331         wmcCounter++;
332         return super.visit(node, data);
333     }
334 
335     @Override
336     public Object visit(ASTWhileStatement node, Object data) {
337         wmcCounter++;
338         return super.visit(node, data);
339     }
340 
341     @Override
342     public Object visit(ASTForStatement node, Object data) {
343         wmcCounter++;
344         return super.visit(node, data);
345     }
346 
347     @Override
348     public Object visit(ASTSwitchLabel node, Object data) {
349         wmcCounter++;
350         return super.visit(node, data);
351     }
352 
353     @Override
354     public Object visit(ASTCatchStatement node, Object data) {
355         wmcCounter++;
356         return super.visit(node, data);
357     }
358 
359     @Override
360     public Object visit(ASTConditionalExpression node, Object data) {
361         if (node.isTernary()) {
362             wmcCounter++;
363         }
364         return super.visit(node, data);
365     }
366 
367 }