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 }