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.strings;
5   
6   import java.util.HashSet;
7   import java.util.List;
8   import java.util.Set;
9   
10  import net.sourceforge.pmd.lang.ast.Node;
11  import net.sourceforge.pmd.lang.java.ast.ASTArgumentList;
12  import net.sourceforge.pmd.lang.java.ast.ASTLiteral;
13  import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration;
14  import net.sourceforge.pmd.lang.java.ast.ASTName;
15  import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
16  import net.sourceforge.pmd.lang.java.symboltable.TypedNameDeclaration;
17  import net.sourceforge.pmd.lang.java.typeresolution.TypeHelper;
18  import net.sourceforge.pmd.lang.symboltable.NameDeclaration;
19  
20  /**
21   * This rule finds places where StringBuffer.toString() is called just to see if
22   * the string is 0 length by either using .equals("") or toString().length()
23   * <p/>
24   * 
25   * <pre>
26   * StringBuffer sb = new StringBuffer(&quot;some string&quot;);
27   * if (sb.toString().equals(&quot;&quot;)) {
28   *     // this is wrong
29   * }
30   * if (sb.length() == 0) {
31   *     // this is right
32   * }
33   * </pre>
34   *
35   * @author acaplan
36   * @author Philip Graf
37   */
38  public class UseStringBufferLengthRule extends AbstractJavaRule {
39  
40      // FIXME Need to remove this somehow.
41      /*
42       * Specifically, we need a symbol tree that can be traversed downwards, so
43       * that instead of visiting each name and then visiting the declaration for
44       * that name, we should visit all the declarations and check their usages.
45       * With that in place, this rule would be reduced to: - find all
46       * StringBuffer declarations - check each usage - flag those that involve
47       * variable.toString()
48       */
49      private Set<NameDeclaration> alreadySeen = new HashSet<>();
50  
51      @Override
52      public Object visit(ASTMethodDeclaration acu, Object data) {
53          alreadySeen.clear();
54          return super.visit(acu, data);
55      }
56  
57      @Override
58      public Object visit(ASTName decl, Object data) {
59          if (!decl.getImage().endsWith("toString")) {
60              return data;
61          }
62          NameDeclaration nd = decl.getNameDeclaration();
63          if (nd == null) {
64              return data;
65          }
66          if (alreadySeen.contains(nd) || !(nd instanceof TypedNameDeclaration) || nd instanceof TypedNameDeclaration
67                  && TypeHelper.isNeither((TypedNameDeclaration) nd, StringBuffer.class, StringBuilder.class)) {
68              return data;
69          }
70          alreadySeen.add(nd);
71  
72          if (isViolation(decl)) {
73              addViolation(data, decl);
74          }
75  
76          return data;
77      }
78  
79      /**
80       * Returns true for the following violations:
81       * 
82       * <pre>
83       * StringBuffer sb = new StringBuffer(&quot;some string&quot;);
84       * if (sb.toString().equals(&quot;&quot;)) {
85       *     // this is a violation
86       * }
87       * if (sb.toString().length() == 0) {
88       *     // this is a violation
89       * }
90       * if (sb.length() == 0) {
91       *     // this is ok
92       * }
93       * </pre>
94       */
95      private boolean isViolation(ASTName decl) {
96          // the (grand)parent of a violation has four children
97          Node parent = decl.jjtGetParent().jjtGetParent();
98          if (parent.jjtGetNumChildren() == 4) {
99              // 1. child: sb.toString where sb is a VariableNameDeclaration for a
100             // StringBuffer or StringBuilder
101             if (parent.jjtGetChild(0).getFirstChildOfType(ASTName.class).getImage().endsWith(".toString")) {
102                 // 2. child: the arguments of toString
103                 // no need to check as both StringBuffer and StringBuilder only
104                 // have one toString method
105                 // 3. child: equals or length, 4. child: their arguments
106                 return isEqualsViolation(parent) || isLengthViolation(parent);
107             }
108         }
109         return false;
110     }
111 
112     private boolean isEqualsViolation(Node parent) {
113         // 3. child: equals
114         if (parent.jjtGetChild(2).hasImageEqualTo("equals")) {
115             // 4. child: the arguments of equals, there must be exactly one and
116             // it must be ""
117             List<ASTArgumentList> argList = parent.jjtGetChild(3).findDescendantsOfType(ASTArgumentList.class);
118             if (argList.size() == 1) {
119                 List<ASTLiteral> literals = argList.get(0).findDescendantsOfType(ASTLiteral.class);
120                 return literals.size() == 1 && literals.get(0).hasImageEqualTo("\"\"");
121             }
122         }
123         return false;
124     }
125 
126     private boolean isLengthViolation(Node parent) {
127         // 3. child: length
128         return parent.jjtGetChild(2).hasImageEqualTo("length");
129         // 4. child: the arguments of length
130         // no need to check as String has only one length method
131     }
132 
133 }