ConsecutiveLiteralAppendsRule.java
001 /**
002  * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
003  */
004 package net.sourceforge.pmd.lang.java.rule.strings;
005 
006 import java.util.HashSet;
007 import java.util.List;
008 import java.util.Map;
009 import java.util.Set;
010 
011 import net.sourceforge.pmd.lang.ast.Node;
012 import net.sourceforge.pmd.lang.java.ast.ASTAdditiveExpression;
013 import net.sourceforge.pmd.lang.java.ast.ASTArgumentList;
014 import net.sourceforge.pmd.lang.java.ast.ASTDoStatement;
015 import net.sourceforge.pmd.lang.java.ast.ASTForStatement;
016 import net.sourceforge.pmd.lang.java.ast.ASTIfStatement;
017 import net.sourceforge.pmd.lang.java.ast.ASTLiteral;
018 import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration;
019 import net.sourceforge.pmd.lang.java.ast.ASTName;
020 import net.sourceforge.pmd.lang.java.ast.ASTPrimaryExpression;
021 import net.sourceforge.pmd.lang.java.ast.ASTPrimarySuffix;
022 import net.sourceforge.pmd.lang.java.ast.ASTSwitchLabel;
023 import net.sourceforge.pmd.lang.java.ast.ASTSwitchStatement;
024 import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclaratorId;
025 import net.sourceforge.pmd.lang.java.ast.ASTWhileStatement;
026 import net.sourceforge.pmd.lang.java.ast.TypeNode;
027 import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
028 import net.sourceforge.pmd.lang.java.symboltable.NameOccurrence;
029 import net.sourceforge.pmd.lang.java.symboltable.VariableNameDeclaration;
030 import net.sourceforge.pmd.lang.java.typeresolution.TypeHelper;
031 import net.sourceforge.pmd.lang.rule.properties.IntegerProperty;
032 
033 /**
034  * This rule finds concurrent calls to StringBuffer.append where String literals
035  * are used It would be much better to make these calls using one call to
036  * .append
037  <p/>
038  * example:
039  <p/>
040  <pre>
041  * StringBuffer buf = new StringBuffer();
042  * buf.append(&quot;Hello&quot;);
043  * buf.append(&quot; &quot;).append(&quot;World&quot;);
044  </pre>
045  <p/>
046  * This would be more eloquently put as:
047  <p/>
048  <pre>
049  * StringBuffer buf = new StringBuffer();
050  * buf.append(&quot;Hello World&quot;);
051  </pre>
052  <p/>
053  * The rule takes one parameter, threshold, which defines the lower limit of
054  * consecutive appends before a violation is created. The default is 1.
055  */
056 public class ConsecutiveLiteralAppendsRule extends AbstractJavaRule {
057 
058     private final static Set<Class<?>> BLOCK_PARENTS;
059 
060     static {
061   BLOCK_PARENTS = new HashSet<Class<?>>();
062   BLOCK_PARENTS.add(ASTForStatement.class);
063   BLOCK_PARENTS.add(ASTWhileStatement.class);
064   BLOCK_PARENTS.add(ASTDoStatement.class);
065   BLOCK_PARENTS.add(ASTIfStatement.class);
066   BLOCK_PARENTS.add(ASTSwitchStatement.class);
067   BLOCK_PARENTS.add(ASTMethodDeclaration.class);
068     }
069 
070     private static final IntegerProperty THRESHOLD_DESCRIPTOR = new IntegerProperty("threshold""Max consecutive appends"11011.0f);
071 
072     private int threshold = 1;
073     
074     public ConsecutiveLiteralAppendsRule() {
075   definePropertyDescriptor(THRESHOLD_DESCRIPTOR);
076     }
077 
078     @Override
079     public Object visit(ASTVariableDeclaratorId node, Object data) {
080 
081   if (!isStringBuffer(node)) {
082       return data;
083   }
084   threshold = getProperty(THRESHOLD_DESCRIPTOR);
085 
086   int concurrentCount = checkConstructor(node, data);
087   Node lastBlock = getFirstParentBlock(node);
088   Node currentBlock = lastBlock;
089   Map<VariableNameDeclaration, List<NameOccurrence>> decls = node.getScope().getVariableDeclarations();
090   Node rootNode = null;
091   // only want the constructor flagged if it's really containing strings
092   if (concurrentCount >= 1) {
093       rootNode = node;
094   }
095   for (Map.Entry<VariableNameDeclaration, List<NameOccurrence>> entry : decls.entrySet()) {
096       List<NameOccurrence> decl = entry.getValue();
097       for (NameOccurrence no : decl) {
098     Node n = no.getLocation();
099 
100     currentBlock = getFirstParentBlock(n);
101 
102     if (!InefficientStringBufferingRule.isInStringBufferOperation(n, 3"append")) {
103         if (!no.isPartOfQualifiedName()) {
104       checkForViolation(rootNode, data, concurrentCount);
105       concurrentCount = 0;
106         }
107         continue;
108     }
109     ASTPrimaryExpression s = n.getFirstParentOfType(ASTPrimaryExpression.class);
110     int numChildren = s.jjtGetNumChildren();
111     for (int jx = 0; jx < numChildren; jx++) {
112         Node sn = s.jjtGetChild(jx);
113         if (!(sn instanceof ASTPrimarySuffix|| sn.getImage() != null) {
114       continue;
115         }
116 
117         // see if it changed blocks
118         if (currentBlock != null && lastBlock != null && !currentBlock.equals(lastBlock)
119           || currentBlock == null ^ lastBlock == null) {
120       checkForViolation(rootNode, data, concurrentCount);
121       concurrentCount = 0;
122         }
123 
124         // if concurrent is 0 then we reset the root to report from
125         // here
126         if (concurrentCount == 0) {
127       rootNode = sn;
128         }
129         if (isAdditive(sn)) {
130       concurrentCount = processAdditive(data, concurrentCount, sn, rootNode);
131       if (concurrentCount != 0) {
132           rootNode = sn;
133       }
134         else if (!isAppendingStringLiteral(sn)) {
135       checkForViolation(rootNode, data, concurrentCount);
136       concurrentCount = 0;
137         else {
138       concurrentCount++;
139         }
140         lastBlock = currentBlock;
141     }
142       }
143   }
144   checkForViolation(rootNode, data, concurrentCount);
145   return data;
146     }
147 
148     /**
149      * Determine if the constructor contains (or ends with) a String Literal
150      *
151      @param node
152      @return 1 if the constructor contains string argument, else 0
153      */
154     private int checkConstructor(ASTVariableDeclaratorId node, Object data) {
155   Node parent = node.jjtGetParent();
156   if (parent.jjtGetNumChildren() >= 2) {
157       ASTArgumentList list = parent.jjtGetChild(1).getFirstDescendantOfType(ASTArgumentList.class);
158       if (list != null) {
159     ASTLiteral literal = list.getFirstDescendantOfType(ASTLiteral.class);
160     if (!isAdditive(list&& literal != null && literal.isStringLiteral()) {
161         return 1;
162     }
163     return processAdditive(data, 0, list, node);
164       }
165   }
166   return 0;
167     }
168 
169     private int processAdditive(Object data, int concurrentCount, Node sn, Node rootNode) {
170   ASTAdditiveExpression additive = sn.getFirstDescendantOfType(ASTAdditiveExpression.class);
171         // The additive expression must of be type String to count
172         if (additive == null || additive.getType() != null && !TypeHelper.isA(additive, String.class)) {
173       return 0;
174   }
175   int count = concurrentCount;
176   boolean found = false;
177   for (int ix = 0; ix < additive.jjtGetNumChildren(); ix++) {
178       Node childNode = additive.jjtGetChild(ix);
179       if (childNode.jjtGetNumChildren() != || childNode.hasDescendantOfType(ASTName.class)) {
180     if (!found) {
181         checkForViolation(rootNode, data, count);
182         found = true;
183     }
184     count = 0;
185       else {
186     count++;
187       }
188   }
189 
190   // no variables appended, compiler will take care of merging all the
191   // string concats, we really only have 1 then
192   if (!found) {
193       count = 1;
194   }
195 
196   return count;
197     }
198 
199     /**
200      * Checks to see if there is string concatenation in the node.
201      *
202      * This method checks if it's additive with respect to the append method
203      * only.
204      *
205      @param n
206      *            Node to check
207      @return true if the node has an additive expression (i.e. "Hello " +
208      *         Const.WORLD)
209      */
210     private boolean isAdditive(Node n) {
211   List<ASTAdditiveExpression> lstAdditive = n.findDescendantsOfType(ASTAdditiveExpression.class);
212   if (lstAdditive.isEmpty()) {
213       return false;
214   }
215   // if there are more than 1 set of arguments above us we're not in the
216   // append
217   // but a sub-method call
218   for (int ix = 0; ix < lstAdditive.size(); ix++) {
219       ASTAdditiveExpression expr = lstAdditive.get(ix);
220       if (expr.getParentsOfType(ASTArgumentList.class).size() != 1) {
221     return false;
222       }
223   }
224   return true;
225     }
226 
227     /**
228      * Get the first parent. Keep track of the last node though. For If
229      * statements it's the only way we can differentiate between if's and else's
230      * For switches it's the only way we can differentiate between switches
231      *
232      @param node The node to check
233      @return The first parent block
234      */
235     private Node getFirstParentBlock(Node node) {
236   Node parentNode = node.jjtGetParent();
237 
238   Node lastNode = node;
239   while (parentNode != null && !BLOCK_PARENTS.contains(parentNode.getClass())) {
240       lastNode = parentNode;
241       parentNode = parentNode.jjtGetParent();
242   }
243   if (parentNode instanceof ASTIfStatement) {
244       parentNode = lastNode;
245   else if (parentNode instanceof ASTSwitchStatement) {
246       parentNode = getSwitchParent(parentNode, lastNode);
247   }
248   return parentNode;
249     }
250 
251     /**
252      * Determine which SwitchLabel we belong to inside a switch
253      *
254      @param parentNode The parent node we're looking at
255      @param lastNode   The last node processed
256      @return The parent node for the switch statement
257      */
258     private Node getSwitchParent(Node parentNode, Node lastNode) {
259   int allChildren = parentNode.jjtGetNumChildren();
260   ASTSwitchLabel label = null;
261   for (int ix = 0; ix < allChildren; ix++) {
262       Node n = parentNode.jjtGetChild(ix);
263       if (instanceof ASTSwitchLabel) {
264     label = (ASTSwitchLabeln;
265       else if (n.equals(lastNode)) {
266     parentNode = label;
267     break;
268       }
269   }
270   return parentNode;
271     }
272 
273     /**
274      * Helper method checks to see if a violation occured, and adds a
275      * RuleViolation if it did
276      */
277     private void checkForViolation(Node node, Object data, int concurrentCount) {
278   if (concurrentCount > threshold) {
279       String[] param = String.valueOf(concurrentCount) };
280       addViolation(data, node, param);
281   }
282     }
283 
284     private boolean isAppendingStringLiteral(Node node) {
285   Node n = node;
286   while (n.jjtGetNumChildren() != && !(instanceof ASTLiteral)) {
287       n = n.jjtGetChild(0);
288   }
289   return instanceof ASTLiteral;
290     }
291 
292     private static boolean isStringBuffer(ASTVariableDeclaratorId node) {
293 
294   if (node.getType() != null) {
295       return node.getType().equals(StringBuffer.class);
296   }
297   Node nn = node.getTypeNameNode();
298   if (nn.jjtGetNumChildren() == 0) {
299       return false;
300   }
301   return TypeHelper.isA((TypeNodenn.jjtGetChild(0), StringBuffer.class);
302     }
303 }