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.coupling;
    5
    6   import java.util.ArrayList;
    7   import java.util.Collections;
    8   import java.util.Iterator;
    9   import java.util.List;
    10  import java.util.Set;
    11
    12  import net.sourceforge.pmd.RuleContext;
    13  import net.sourceforge.pmd.lang.java.ast.ASTAllocationExpression;
    14  import net.sourceforge.pmd.lang.java.ast.ASTAssignmentOperator;
    15  import net.sourceforge.pmd.lang.java.ast.ASTBlock;
    16  import net.sourceforge.pmd.lang.java.ast.ASTForStatement;
    17  import net.sourceforge.pmd.lang.java.ast.ASTLiteral;
    18  import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration;
    19  import net.sourceforge.pmd.lang.java.ast.ASTName;
    20  import net.sourceforge.pmd.lang.java.ast.ASTPrimaryExpression;
    21  import net.sourceforge.pmd.lang.java.ast.ASTPrimaryPrefix;
    22  import net.sourceforge.pmd.lang.java.ast.ASTPrimarySuffix;
    23  import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclarator;
    24  import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclaratorId;
    25  import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
    26  import net.sourceforge.pmd.lang.java.symboltable.LocalScope;
    27  import net.sourceforge.pmd.lang.java.symboltable.Scope;
    28  import net.sourceforge.pmd.lang.java.symboltable.VariableNameDeclaration;
    29
    30  /**
    31   * This rule can detect possible violations of the Law of Demeter.
    32   * The Law of Demeter is a simple rule, that says "only talk to friends". It helps to reduce
    33   * coupling between classes or objects.
    34   * <p>
    35   * See:
    36   * <ul>
    37   *   <li>Andrew Hunt, David Thomas, and Ward Cunningham. The Pragmatic Programmer. From Journeyman to Master. Addison-Wesley Longman, Amsterdam, October 1999.</li>
    38   *   <li>K.J. Lieberherr and I.M. Holland. Assuring good style for object-oriented programs. Software, IEEE, 6(5):38–48, 1989.</li>
    39   * </ul>
    40   *
    41   * @since 5.0
    42   *
    43   */
    44  public class LawOfDemeterRule extends AbstractJavaRule {
    45      private static final String REASON_METHOD_CHAIN_CALLS = "method chain calls";
    46      private static final String REASON_OBJECT_NOT_CREATED_LOCALLY = "object not created locally";
    47      private static final String REASON_STATIC_ACCESS = "static property access";
    48
    49      /**
    50       * That's a new method. We are going to check each method call inside the method.
    51       * @return <code>null</code>.
    52       */
    53      @Override
    54      public Object visit(ASTMethodDeclaration node, Object data) {
    55          List<ASTPrimaryExpression> primaryExpressions = node.findDescendantsOfType(ASTPrimaryExpression.class);
    56          for (ASTPrimaryExpression expression : primaryExpressions) {
    57              List<MethodCall> calls = MethodCall.createMethodCalls(expression);
    58              addViolations(calls, (RuleContext)data);
    59          }
    60          return null;
    61      }
    62
    63      private void addViolations(List<MethodCall> calls, RuleContext ctx) {
    64          for (MethodCall method : calls) {
    65              if (method.isViolation()) {
    66                  addViolationWithMessage(ctx, method.getExpression(), getMessage() + " (" + method.getViolationReason() + ")");
    67              }
    68          }
    69      }
    70
    71
    72      /**
    73       * Collects the information of one identified method call. The method call
    74       * might be a violation of the Law of Demeter or not.
    75       */
    76      private static class MethodCall {
    77          private static final String METHOD_CALL_CHAIN = "result from previous method call";
    78          private static final String SIMPLE_ASSIGNMENT_OPERATOR = "=";
    79          private static final String SCOPE_METHOD_CHAINING = "method-chaining";
    80          private static final String SCOPE_CLASS = "class";
    81          private static final String SCOPE_METHOD = "method";
    82          private static final String SCOPE_LOCAL = "local";
    83          private static final String SCOPE_STATIC_CHAIN = "static-chain";
    84          private static final String SUPER = "super";
    85          private static final String THIS = "this";
    86
    87          private ASTPrimaryExpression expression;
    88          private String baseName;
    89          private String methodName;
    90          private String baseScope;
    91          private String baseTypeName;
    92          private Class<?> baseType;
    93          private boolean violation;
    94          private String violationReason;
    95
    96          /**
    97           * Create a new method call for the prefix expression part of the primary expression.
    98           */
    99          private MethodCall(ASTPrimaryExpression expression, ASTPrimaryPrefix prefix) {
    100             this.expression = expression;
    101             analyze(prefix);
    102             determineType();
    103             checkViolation();
    104         }
    105
    106         /**
    107          * Create a new method call for the given suffix expression part of the primary expression.
    108          * This is used for method chains.
    109          */
    110         private MethodCall(ASTPrimaryExpression expression, ASTPrimarySuffix suffix) {
    111             this.expression = expression;
    112             analyze(suffix);
    113             determineType();
    114             checkViolation();
    115         }
    116
    117         /**
    118          * Factory method to convert a given primary expression into MethodCalls.
    119          * In case the primary expression represents a method chain call, then multiple
    120          * MethodCalls are returned.
    121          *
    122          * @return a list of MethodCalls, might be empty.
    123          */
    124         public static List<MethodCall> createMethodCalls(ASTPrimaryExpression expression) {
    125             List<MethodCall> result = new ArrayList<MethodCall>();
    126
    127             if (isNotAConstructorCall(expression) && isNotLiteral(expression) && hasSuffixesWithArguments(expression)) {
    128                 ASTPrimaryPrefix prefixNode = expression.getFirstDescendantOfType(ASTPrimaryPrefix.class);
    129                 MethodCall firstMethodCallInChain = new MethodCall(expression, prefixNode);
    130                 result.add(firstMethodCallInChain);
    131
    132                 if (firstMethodCallInChain.isNotBuilder()) {
    133                     List<ASTPrimarySuffix> suffixes = findSuffixesWithoutArguments(expression);
    134                     for (ASTPrimarySuffix suffix : suffixes) {
    135                         result.add(new MethodCall(expression, suffix));
    136                     }
    137                 }
    138             }
    139
    140             return result;
    141         }
    142
    143         private static boolean isNotAConstructorCall(ASTPrimaryExpression expression) {
    144             return !expression.hasDescendantOfType(ASTAllocationExpression.class);
    145         }
    146
    147         private static boolean isNotLiteral(ASTPrimaryExpression expression) {
    148             ASTPrimaryPrefix prefix = expression.getFirstDescendantOfType(ASTPrimaryPrefix.class);
    149             if (prefix != null) {
    150                 return !prefix.hasDescendantOfType(ASTLiteral.class);
    151             }
    152             return true;
    153         }
    154
    155         private boolean isNotBuilder() {
    156             return baseType != StringBuffer.class
    157                     && baseType != StringBuilder.class
    158                     && !"StringBuilder".equals(baseTypeName)
    159                     && !"StringBuffer".equals(baseTypeName);
    160         }
    161
    162         private static List<ASTPrimarySuffix> findSuffixesWithoutArguments(ASTPrimaryExpression expr) {
    163             List<ASTPrimarySuffix> result = new ArrayList<ASTPrimarySuffix>();
    164             if (hasRealPrefix(expr)) {
    165                 List<ASTPrimarySuffix> suffixes = expr.findDescendantsOfType(ASTPrimarySuffix.class);
    166                 for (ASTPrimarySuffix suffix : suffixes) {
    167                     if (!suffix.isArguments()) {
    168                         result.add(suffix);
    169                     }
    170                 }
    171             }
    172             return result;
    173         }
    174
    175         private static boolean hasRealPrefix(ASTPrimaryExpression expr) {
    176             ASTPrimaryPrefix prefix = expr.getFirstDescendantOfType(ASTPrimaryPrefix.class);
    177             return !prefix.usesThisModifier() && !prefix.usesSuperModifier();
    178         }
    179
    180         private static boolean hasSuffixesWithArguments(ASTPrimaryExpression expr) {
    181             boolean result = false;
    182             if (hasRealPrefix(expr)) {
    183                 List<ASTPrimarySuffix> suffixes = expr.findDescendantsOfType(ASTPrimarySuffix.class);
    184                 for (ASTPrimarySuffix suffix : suffixes) {
    185                     if (suffix.isArguments()) {
    186                         result = true;
    187                         break;
    188                     }
    189                 }
    190             }
    191             return result;
    192         }
    193
    194         private void analyze(ASTPrimaryPrefix prefixNode) {
    195             List<ASTName> names = prefixNode.findDescendantsOfType(ASTName.class);
    196
    197             baseName = "unknown";
    198             methodName = "unknown";
    199
    200             if (!names.isEmpty()) {
    201                 baseName = names.get(0).getImage();
    202
    203                 int dot = baseName.lastIndexOf('.');
    204                 if (dot == -1) {
    205                     methodName = baseName;
    206                     baseName = THIS;
    207                 } else {
    208                     methodName = baseName.substring(dot + 1);
    209                     baseName = baseName.substring(0, dot);
    210                 }
    211
    212             } else {
    213                 if (prefixNode.usesThisModifier()) {
    214                     baseName = THIS;
    215                 } else if (prefixNode.usesSuperModifier()) {
    216                     baseName = SUPER;
    217                 }
    218             }
    219         }
    220
    221         private void analyze(ASTPrimarySuffix suffix) {
    222             baseName = METHOD_CALL_CHAIN;
    223             methodName = suffix.getImage();
    224         }
    225
    226         private void checkViolation() {
    227             violation = false;
    228             violationReason = null;
    229
    230             if (SCOPE_LOCAL.equals(baseScope)) {
    231                 Assignment lastAssignment = determineLastAssignment();
    232                 if (lastAssignment != null
    233                     && !lastAssignment.allocation
    234                     && !lastAssignment.iterator
    235                     && !lastAssignment.forLoop) {
    236                     violation = true;
    237                     violationReason = REASON_OBJECT_NOT_CREATED_LOCALLY;
    238                 }
    239             } else if (SCOPE_METHOD_CHAINING.equals(baseScope)) {
    240                 violation = true;
    241                 violationReason = REASON_METHOD_CHAIN_CALLS;
    242             } else if (SCOPE_STATIC_CHAIN.equals(baseScope)) {
    243                 violation = true;
    244                 violationReason = REASON_STATIC_ACCESS;
    245             }
    246         }
    247
    248         private void determineType() {
    249             VariableNameDeclaration var = null;
    250             Scope scope = expression.getScope();
    251
    252             baseScope = SCOPE_LOCAL;
    253             var = findInLocalScope(baseName, (LocalScope)scope);
    254             if (var == null) {
    255                 baseScope = SCOPE_METHOD;
    256                 var = determineTypeOfVariable(baseName, scope.getEnclosingMethodScope().getVariableDeclarations().keySet());
    257             }
    258             if (var == null) {
    259                 baseScope = SCOPE_CLASS;
    260                 var = determineTypeOfVariable(baseName, scope.getEnclosingClassScope().getVariableDeclarations().keySet());
    261             }
    262             if (var == null) {
    263                 baseScope = SCOPE_METHOD_CHAINING;
    264             }
    265             if (var == null && (THIS.equals(baseName) || SUPER.equals(baseName))) {
    266                 baseScope = SCOPE_CLASS;
    267             }
    268
    269             if (var != null) {
    270                 baseTypeName = var.getTypeImage();
    271                 baseType = var.getType();
    272             } else if (METHOD_CALL_CHAIN.equals(baseName)) {
    273                 baseScope = SCOPE_METHOD_CHAINING;
    274             } else if (baseName.contains(".") && !baseName.startsWith("System.")) {
    275                 baseScope = SCOPE_STATIC_CHAIN;
    276             } else {
    277                 // everything else is no violation - probably a static method call.
    278                 baseScope = null;
    279             }
    280         }
    281
    282         private VariableNameDeclaration findInLocalScope(String name, LocalScope scope) {
    283             VariableNameDeclaration result = null;
    284
    285             result = determineTypeOfVariable(name, scope.getVariableDeclarations().keySet());
    286             if (result == null && scope.getParent() instanceof LocalScope) {
    287                 result = findInLocalScope(name, (LocalScope)scope.getParent());
    288             }
    289
    290             return result;
    291         }
    292
    293         private VariableNameDeclaration determineTypeOfVariable(String variableName, Set<VariableNameDeclaration> declarations) {
    294             VariableNameDeclaration result = null;
    295             for (VariableNameDeclaration var : declarations) {
    296                 if (variableName.equals(var.getImage())) {
    297                     result = var;
    298                     break;
    299                 }
    300             }
    301             return result;
    302         }
    303
    304         private Assignment determineLastAssignment() {
    305             List<Assignment> assignments = new ArrayList<Assignment>();
    306
    307             ASTBlock block = expression.getFirstParentOfType(ASTMethodDeclaration.class).getFirstChildOfType(ASTBlock.class);
    308
    309             List<ASTVariableDeclarator> variableDeclarators = block.findDescendantsOfType(ASTVariableDeclarator.class);
    310             for (ASTVariableDeclarator declarator : variableDeclarators) {
    311                 ASTVariableDeclaratorId variableDeclaratorId = declarator.getFirstChildOfType(ASTVariableDeclaratorId.class);
    312                 if (variableDeclaratorId.hasImageEqualTo(baseName)) {
    313                     boolean allocationFound = declarator.getFirstDescendantOfType(ASTAllocationExpression.class) != null;
    314                     boolean iterator = isIterator();
    315                     boolean forLoop = isForLoop(declarator);
    316                     assignments.add(new Assignment(declarator.getBeginLine(), allocationFound, iterator, forLoop));
    317                 }
    318             }
    319
    320             List<ASTAssignmentOperator> assignmentStmts = block.findDescendantsOfType(ASTAssignmentOperator.class);
    321             for (ASTAssignmentOperator stmt : assignmentStmts) {
    322                 if (stmt.hasImageEqualTo(SIMPLE_ASSIGNMENT_OPERATOR)) {
    323                     boolean allocationFound = stmt.jjtGetParent().getFirstDescendantOfType(ASTAllocationExpression.class) != null;
    324                     boolean iterator = isIterator();
    325                     assignments.add(new Assignment(stmt.getBeginLine(), allocationFound, iterator, false));
    326                 }
    327             }
    328
    329             Assignment result = null;
    330             if (!assignments.isEmpty()) {
    331                 Collections.sort(assignments);
    332                 result = assignments.get(0);
    333             }
    334             return result;
    335         }
    336
    337         private boolean isIterator() {
    338             boolean iterator = false;
    339             if ((baseType != null && baseType == Iterator.class)
    340                     || (baseTypeName != null && baseTypeName.endsWith("Iterator"))) {
    341                 iterator = true;
    342             }
    343             return iterator;
    344         }
    345
    346         private boolean isForLoop(ASTVariableDeclarator declarator) {
    347             return declarator.jjtGetParent().jjtGetParent() instanceof ASTForStatement;
    348         }
    349
    350         public ASTPrimaryExpression getExpression() {
    351             return expression;
    352         }
    353
    354         public boolean isViolation() {
    355             return violation;
    356         }
    357
    358         public String getViolationReason() {
    359             return violationReason;
    360         }
    361
    362         @Override
    363         public String toString() {
    364             return "MethodCall on line " + expression.getBeginLine() + ":\n"
    365                 + "  " + baseName + " name: "+ methodName+ "\n"
    366                 + "  type: " + baseTypeName + " (" + baseType + "), \n"
    367                 + "  scope: " + baseScope + "\n"
    368                 + "  violation: " + violation + " (" + violationReason + ")\n";
    369         }
    370
    371     }
    372
    373     /**
    374      * Stores the assignment of a variable and whether the variable's value is
    375      * allocated locally (new constructor call). The class is comparable, so that
    376      * the last assignment can be determined.
    377      */
    378     private static class Assignment implements Comparable<Assignment> {
    379         private int line;
    380         private boolean allocation;
    381         private boolean iterator;
    382         private boolean forLoop;
    383
    384         public Assignment(int line, boolean allocation, boolean iterator, boolean forLoop) {
    385             this.line = line;
    386             this.allocation = allocation;
    387             this.iterator = iterator;
    388             this.forLoop = forLoop;
    389         }
    390
    391         @Override
    392         public String toString() {
    393             return "assignment: line=" + line + " allocation:" + allocation
    394                 + " iterator:" + iterator + " forLoop: " + forLoop;
    395         }
    396
    397         public int compareTo(Assignment o) {
    398             return o.line - line;
    399         }
    400     }
    401 }