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.Map;
9   import java.util.Set;
10  
11  import net.sourceforge.pmd.lang.ast.Node;
12  import net.sourceforge.pmd.lang.java.ast.ASTAdditiveExpression;
13  import net.sourceforge.pmd.lang.java.ast.ASTAllocationExpression;
14  import net.sourceforge.pmd.lang.java.ast.ASTArgumentList;
15  import net.sourceforge.pmd.lang.java.ast.ASTDoStatement;
16  import net.sourceforge.pmd.lang.java.ast.ASTForStatement;
17  import net.sourceforge.pmd.lang.java.ast.ASTIfStatement;
18  import net.sourceforge.pmd.lang.java.ast.ASTLiteral;
19  import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration;
20  import net.sourceforge.pmd.lang.java.ast.ASTName;
21  import net.sourceforge.pmd.lang.java.ast.ASTPrimaryExpression;
22  import net.sourceforge.pmd.lang.java.ast.ASTPrimarySuffix;
23  import net.sourceforge.pmd.lang.java.ast.ASTSwitchLabel;
24  import net.sourceforge.pmd.lang.java.ast.ASTSwitchStatement;
25  import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclaratorId;
26  import net.sourceforge.pmd.lang.java.ast.ASTVariableInitializer;
27  import net.sourceforge.pmd.lang.java.ast.ASTWhileStatement;
28  import net.sourceforge.pmd.lang.java.ast.TypeNode;
29  import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
30  import net.sourceforge.pmd.lang.java.symboltable.JavaNameOccurrence;
31  import net.sourceforge.pmd.lang.java.symboltable.VariableNameDeclaration;
32  import net.sourceforge.pmd.lang.java.typeresolution.TypeHelper;
33  import net.sourceforge.pmd.lang.rule.properties.IntegerProperty;
34  import net.sourceforge.pmd.lang.symboltable.NameOccurrence;
35  
36  /**
37   * This rule finds concurrent calls to StringBuffer/Builder.append where String
38   * literals are used It would be much better to make these calls using one call
39   * to .append
40   * <p/>
41   * example:
42   * <p/>
43   * 
44   * <pre>
45   * StringBuilder buf = new StringBuilder();
46   * buf.append(&quot;Hello&quot;);
47   * buf.append(&quot; &quot;).append(&quot;World&quot;);
48   * </pre>
49   * <p/>
50   * This would be more eloquently put as:
51   * <p/>
52   * 
53   * <pre>
54   * StringBuilder buf = new StringBuilder();
55   * buf.append(&quot;Hello World&quot;);
56   * </pre>
57   * <p/>
58   * The rule takes one parameter, threshold, which defines the lower limit of
59   * consecutive appends before a violation is created. The default is 1.
60   */
61  public class ConsecutiveLiteralAppendsRule extends AbstractJavaRule {
62  
63      private final static Set<Class<?>> BLOCK_PARENTS;
64  
65      static {
66          BLOCK_PARENTS = new HashSet<>();
67          BLOCK_PARENTS.add(ASTForStatement.class);
68          BLOCK_PARENTS.add(ASTWhileStatement.class);
69          BLOCK_PARENTS.add(ASTDoStatement.class);
70          BLOCK_PARENTS.add(ASTIfStatement.class);
71          BLOCK_PARENTS.add(ASTSwitchStatement.class);
72          BLOCK_PARENTS.add(ASTMethodDeclaration.class);
73      }
74  
75      private static final IntegerProperty THRESHOLD_DESCRIPTOR = new IntegerProperty("threshold",
76              "Max consecutive appends", 1, 10, 1, 1.0f);
77  
78      private int threshold = 1;
79  
80      public ConsecutiveLiteralAppendsRule() {
81          definePropertyDescriptor(THRESHOLD_DESCRIPTOR);
82      }
83  
84      @Override
85      public Object visit(ASTVariableDeclaratorId node, Object data) {
86  
87          if (!isStringBuffer(node)) {
88              return data;
89          }
90          threshold = getProperty(THRESHOLD_DESCRIPTOR);
91  
92          int concurrentCount = checkConstructor(node, data);
93          if (hasInitializer(node)) {
94              concurrentCount += checkInitializerExpressions(node);
95          }
96          Node lastBlock = getFirstParentBlock(node);
97          Node currentBlock = lastBlock;
98          Map<VariableNameDeclaration, List<NameOccurrence>> decls = node.getScope().getDeclarations(
99                  VariableNameDeclaration.class);
100         Node rootNode = null;
101         // only want the constructor flagged if it's really containing strings
102         if (concurrentCount >= 1) {
103             rootNode = node;
104         }
105         for (List<NameOccurrence> decl : decls.values()) {
106             for (NameOccurrence no : decl) {
107                 JavaNameOccurrence jno = (JavaNameOccurrence) no;
108                 Node n = jno.getLocation();
109 
110                 // skip the declarations/usages, that deal with a different variable
111                 if (!node.getImage().equals(jno.getImage())) {
112                     continue;
113                 }
114 
115                 currentBlock = getFirstParentBlock(n);
116 
117                 if (!InefficientStringBufferingRule.isInStringBufferOperation(n, 3, "append")) {
118                     if (!jno.isPartOfQualifiedName()) {
119                         checkForViolation(rootNode, data, concurrentCount);
120                         concurrentCount = 0;
121                     }
122                     continue;
123                 }
124                 ASTPrimaryExpression s = n.getFirstParentOfType(ASTPrimaryExpression.class);
125                 int numChildren = s.jjtGetNumChildren();
126                 for (int jx = 0; jx < numChildren; jx++) {
127                     Node sn = s.jjtGetChild(jx);
128                     if (!(sn instanceof ASTPrimarySuffix) || sn.getImage() != null) {
129                         continue;
130                     }
131 
132                     // see if it changed blocks
133                     if (currentBlock != null && lastBlock != null && !currentBlock.equals(lastBlock)
134                             || currentBlock == null ^ lastBlock == null) {
135                         checkForViolation(rootNode, data, concurrentCount);
136                         concurrentCount = 0;
137                     }
138 
139                     // if concurrent is 0 then we reset the root to report from
140                     // here
141                     if (concurrentCount == 0) {
142                         rootNode = sn;
143                     }
144                     if (isAdditive(sn)) {
145                         concurrentCount = processAdditive(data, concurrentCount, sn, rootNode);
146                         if (concurrentCount != 0) {
147                             rootNode = sn;
148                         }
149                     } else if (!isAppendingStringLiteral(sn)) {
150                         checkForViolation(rootNode, data, concurrentCount);
151                         concurrentCount = 0;
152                     } else {
153                         concurrentCount++;
154                     }
155                     lastBlock = currentBlock;
156                 }
157             }
158         }
159         checkForViolation(rootNode, data, concurrentCount);
160         return data;
161     }
162 
163     /**
164      * Determine if the constructor contains (or ends with) a String Literal
165      *
166      * @param node
167      * @return 1 if the constructor contains string argument, else 0
168      */
169     private int checkConstructor(ASTVariableDeclaratorId node, Object data) {
170         Node parent = node.jjtGetParent();
171         if (parent.jjtGetNumChildren() >= 2) {
172             ASTAllocationExpression allocationExpression = parent.jjtGetChild(1).getFirstDescendantOfType(ASTAllocationExpression.class);
173             ASTArgumentList list = null;
174             if (allocationExpression != null) {
175                 list = allocationExpression.getFirstDescendantOfType(ASTArgumentList.class);
176             }
177 
178             if (list != null) {
179                 ASTLiteral literal = list.getFirstDescendantOfType(ASTLiteral.class);
180                 if (!isAdditive(list) && literal != null && literal.isStringLiteral()) {
181                     return 1;
182                 }
183                 return processAdditive(data, 0, list, node);
184             }
185         }
186         return 0;
187     }
188 
189     /**
190      * Determine if during the variable initializer calls to ".append" are done.
191      *
192      * @param node
193      * @return
194      */
195     private int checkInitializerExpressions(ASTVariableDeclaratorId node) {
196         ASTVariableInitializer initializer = node.jjtGetParent().getFirstChildOfType(ASTVariableInitializer.class);
197         ASTPrimaryExpression primary = initializer.getFirstDescendantOfType(ASTPrimaryExpression.class);
198 
199         int result = 0;
200         boolean previousWasAppend = false;
201         for (int i = 0; i < primary.jjtGetNumChildren(); i++) {
202             Node child = primary.jjtGetChild(i);
203             if (child.jjtGetNumChildren() > 0 && child.jjtGetChild(0) instanceof ASTAllocationExpression) {
204                 continue; // skip the constructor call, that has already been checked
205             }
206             if (child instanceof ASTPrimarySuffix) {
207                 ASTPrimarySuffix suffix = (ASTPrimarySuffix)child;
208                 if (suffix.jjtGetNumChildren() == 0 && suffix.hasImageEqualTo("append")) {
209                     previousWasAppend = true;
210                 } else if (suffix.jjtGetNumChildren() > 0 && previousWasAppend) {
211                     previousWasAppend = false;
212 
213                     ASTLiteral literal = suffix.getFirstDescendantOfType(ASTLiteral.class);
214                     if (literal != null && literal.isStringLiteral()) {
215                         result++;
216                     } else {
217                         // if it was not a String literal that was appended, then we don't
218                         // have a consecutive literal string append anymore and we can skip
219                         // checking the remainder of the initializer
220                         break;
221                     }
222                 }
223             }
224         }
225 
226         return result;
227     }
228 
229     private boolean hasInitializer(ASTVariableDeclaratorId node) {
230         return node.jjtGetParent().hasDescendantOfType(ASTVariableInitializer.class);
231     }
232 
233     private int processAdditive(Object data, int concurrentCount, Node sn, Node rootNode) {
234         ASTAdditiveExpression additive = sn.getFirstDescendantOfType(ASTAdditiveExpression.class);
235         // The additive expression must of be type String to count
236         if (additive == null || additive.getType() != null && !TypeHelper.isA(additive, String.class)) {
237             return 0;
238         }
239         // check for at least one string literal
240         List<ASTLiteral> literals = additive.findDescendantsOfType(ASTLiteral.class);
241         boolean stringLiteralFound = false;
242         for (ASTLiteral l : literals) {
243             if (l.isCharLiteral() || l.isStringLiteral()) {
244                 stringLiteralFound = true;
245                 break;
246             }
247         }
248         if (!stringLiteralFound) {
249             return 0;
250         }
251 
252         int count = concurrentCount;
253         boolean found = false;
254         for (int ix = 0; ix < additive.jjtGetNumChildren(); ix++) {
255             Node childNode = additive.jjtGetChild(ix);
256             if (childNode.jjtGetNumChildren() != 1 || childNode.hasDescendantOfType(ASTName.class)) {
257                 if (!found) {
258                     checkForViolation(rootNode, data, count);
259                     found = true;
260                 }
261                 count = 0;
262             } else {
263                 count++;
264             }
265         }
266 
267         // no variables appended, compiler will take care of merging all the
268         // string concats, we really only have 1 then
269         if (!found) {
270             count = 1;
271         }
272 
273         return count;
274     }
275 
276     /**
277      * Checks to see if there is string concatenation in the node.
278      *
279      * This method checks if it's additive with respect to the append method
280      * only.
281      *
282      * @param n
283      *            Node to check
284      * @return true if the node has an additive expression (i.e. "Hello " +
285      *         Const.WORLD)
286      */
287     private boolean isAdditive(Node n) {
288         List<ASTAdditiveExpression> lstAdditive = n.findDescendantsOfType(ASTAdditiveExpression.class);
289         if (lstAdditive.isEmpty()) {
290             return false;
291         }
292         // if there are more than 1 set of arguments above us we're not in the
293         // append
294         // but a sub-method call
295         for (int ix = 0; ix < lstAdditive.size(); ix++) {
296             ASTAdditiveExpression expr = lstAdditive.get(ix);
297             if (expr.getParentsOfType(ASTArgumentList.class).size() != 1) {
298                 return false;
299             }
300         }
301         return true;
302     }
303 
304     /**
305      * Get the first parent. Keep track of the last node though. For If
306      * statements it's the only way we can differentiate between if's and else's
307      * For switches it's the only way we can differentiate between switches
308      *
309      * @param node
310      *            The node to check
311      * @return The first parent block
312      */
313     private Node getFirstParentBlock(Node node) {
314         Node parentNode = node.jjtGetParent();
315 
316         Node lastNode = node;
317         while (parentNode != null && !BLOCK_PARENTS.contains(parentNode.getClass())) {
318             lastNode = parentNode;
319             parentNode = parentNode.jjtGetParent();
320         }
321         if (parentNode instanceof ASTIfStatement) {
322             parentNode = lastNode;
323         } else if (parentNode instanceof ASTSwitchStatement) {
324             parentNode = getSwitchParent(parentNode, lastNode);
325         }
326         return parentNode;
327     }
328 
329     /**
330      * Determine which SwitchLabel we belong to inside a switch
331      *
332      * @param parentNode
333      *            The parent node we're looking at
334      * @param lastNode
335      *            The last node processed
336      * @return The parent node for the switch statement
337      */
338     private Node getSwitchParent(Node parentNode, Node lastNode) {
339         int allChildren = parentNode.jjtGetNumChildren();
340         Node result = parentNode;
341         ASTSwitchLabel label = null;
342         for (int ix = 0; ix < allChildren; ix++) {
343             Node n = result.jjtGetChild(ix);
344             if (n instanceof ASTSwitchLabel) {
345                 label = (ASTSwitchLabel) n;
346             } else if (n.equals(lastNode)) {
347                 result = label;
348                 break;
349             }
350         }
351         return result;
352     }
353 
354     /**
355      * Helper method checks to see if a violation occurred, and adds a
356      * RuleViolation if it did
357      */
358     private void checkForViolation(Node node, Object data, int concurrentCount) {
359         if (concurrentCount > threshold) {
360             String[] param = { String.valueOf(concurrentCount) };
361             addViolation(data, node, param);
362         }
363     }
364 
365     private boolean isAppendingStringLiteral(Node node) {
366         Node n = node;
367         while (n.jjtGetNumChildren() != 0 && !(n instanceof ASTLiteral)) {
368             n = n.jjtGetChild(0);
369         }
370         return n instanceof ASTLiteral;
371     }
372 
373     private static boolean isStringBuffer(ASTVariableDeclaratorId node) {
374 
375         if (node.getType() != null) {
376             // return node.getType().equals(StringBuffer.class);
377             return TypeHelper.isEither(node, StringBuffer.class, StringBuilder.class);
378         }
379         Node nn = node.getTypeNameNode();
380         if (nn == null || nn.jjtGetNumChildren() == 0) {
381             return false;
382         }
383         return TypeHelper.isEither((TypeNode) nn.jjtGetChild(0), StringBuffer.class, StringBuilder.class);
384     }
385 }