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<Class<?>>();
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          concurrentCount += checkInitializerExpressions(node);
94          Node lastBlock = getFirstParentBlock(node);
95          Node currentBlock = lastBlock;
96          Map<VariableNameDeclaration, List<NameOccurrence>> decls = node.getScope().getDeclarations(
97                  VariableNameDeclaration.class);
98          Node rootNode = null;
99          // only want the constructor flagged if it's really containing strings
100         if (concurrentCount >= 1) {
101             rootNode = node;
102         }
103         for (List<NameOccurrence> decl : decls.values()) {
104             for (NameOccurrence no : decl) {
105                 JavaNameOccurrence jno = (JavaNameOccurrence) no;
106                 Node n = jno.getLocation();
107 
108                 // skip the declarations/usages, that deal with a different variable
109                 if (!node.getImage().equals(jno.getImage())) {
110                     continue;
111                 }
112 
113                 currentBlock = getFirstParentBlock(n);
114 
115                 if (!InefficientStringBufferingRule.isInStringBufferOperation(n, 3, "append")) {
116                     if (!jno.isPartOfQualifiedName()) {
117                         checkForViolation(rootNode, data, concurrentCount);
118                         concurrentCount = 0;
119                     }
120                     continue;
121                 }
122                 ASTPrimaryExpression s = n.getFirstParentOfType(ASTPrimaryExpression.class);
123                 int numChildren = s.jjtGetNumChildren();
124                 for (int jx = 0; jx < numChildren; jx++) {
125                     Node sn = s.jjtGetChild(jx);
126                     if (!(sn instanceof ASTPrimarySuffix) || sn.getImage() != null) {
127                         continue;
128                     }
129 
130                     // see if it changed blocks
131                     if (currentBlock != null && lastBlock != null && !currentBlock.equals(lastBlock)
132                             || currentBlock == null ^ lastBlock == null) {
133                         checkForViolation(rootNode, data, concurrentCount);
134                         concurrentCount = 0;
135                     }
136 
137                     // if concurrent is 0 then we reset the root to report from
138                     // here
139                     if (concurrentCount == 0) {
140                         rootNode = sn;
141                     }
142                     if (isAdditive(sn)) {
143                         concurrentCount = processAdditive(data, concurrentCount, sn, rootNode);
144                         if (concurrentCount != 0) {
145                             rootNode = sn;
146                         }
147                     } else if (!isAppendingStringLiteral(sn)) {
148                         checkForViolation(rootNode, data, concurrentCount);
149                         concurrentCount = 0;
150                     } else {
151                         concurrentCount++;
152                     }
153                     lastBlock = currentBlock;
154                 }
155             }
156         }
157         checkForViolation(rootNode, data, concurrentCount);
158         return data;
159     }
160 
161     /**
162      * Determine if the constructor contains (or ends with) a String Literal
163      *
164      * @param node
165      * @return 1 if the constructor contains string argument, else 0
166      */
167     private int checkConstructor(ASTVariableDeclaratorId node, Object data) {
168         Node parent = node.jjtGetParent();
169         if (parent.jjtGetNumChildren() >= 2) {
170             ASTAllocationExpression allocationExpression = parent.jjtGetChild(1).getFirstDescendantOfType(ASTAllocationExpression.class);
171             ASTArgumentList list = null;
172             if (allocationExpression != null) {
173                 list = allocationExpression.getFirstDescendantOfType(ASTArgumentList.class);
174             }
175 
176             if (list != null) {
177                 ASTLiteral literal = list.getFirstDescendantOfType(ASTLiteral.class);
178                 if (!isAdditive(list) && literal != null && literal.isStringLiteral()) {
179                     return 1;
180                 }
181                 return processAdditive(data, 0, list, node);
182             }
183         }
184         return 0;
185     }
186 
187     /**
188      * Determine if during the variable initializer calls to ".append" are done.
189      *
190      * @param node
191      * @return
192      */
193     private int checkInitializerExpressions(ASTVariableDeclaratorId node) {
194         ASTVariableInitializer initializer = node.jjtGetParent().getFirstChildOfType(ASTVariableInitializer.class);
195         ASTPrimaryExpression primary = initializer.getFirstDescendantOfType(ASTPrimaryExpression.class);
196 
197         int result = 0;
198         boolean previousWasAppend = false;
199         for (int i = 0; i < primary.jjtGetNumChildren(); i++) {
200             Node child = primary.jjtGetChild(i);
201             if (child.jjtGetNumChildren() > 0 && child.jjtGetChild(0) instanceof ASTAllocationExpression) {
202                 continue; // skip the constructor call, that has already been checked
203             }
204             if (child instanceof ASTPrimarySuffix) {
205                 ASTPrimarySuffix suffix = (ASTPrimarySuffix)child;
206                 if (suffix.jjtGetNumChildren() == 0 && suffix.hasImageEqualTo("append")) {
207                     previousWasAppend = true;
208                 } else if (suffix.jjtGetNumChildren() > 0 && previousWasAppend) {
209                     previousWasAppend = false;
210 
211                     ASTLiteral literal = suffix.getFirstDescendantOfType(ASTLiteral.class);
212                     if (literal != null && literal.isStringLiteral()) {
213                         result++;
214                     }
215                 }
216             }
217         }
218 
219         return result;
220     }
221 
222     private int processAdditive(Object data, int concurrentCount, Node sn, Node rootNode) {
223         ASTAdditiveExpression additive = sn.getFirstDescendantOfType(ASTAdditiveExpression.class);
224         // The additive expression must of be type String to count
225         if (additive == null || additive.getType() != null && !TypeHelper.isA(additive, String.class)) {
226             return 0;
227         }
228         // check for at least one string literal
229         List<ASTLiteral> literals = additive.findDescendantsOfType(ASTLiteral.class);
230         boolean stringLiteralFound = false;
231         for (ASTLiteral l : literals) {
232             if (l.isCharLiteral() || l.isStringLiteral()) {
233                 stringLiteralFound = true;
234                 break;
235             }
236         }
237         if (!stringLiteralFound) {
238             return 0;
239         }
240 
241         int count = concurrentCount;
242         boolean found = false;
243         for (int ix = 0; ix < additive.jjtGetNumChildren(); ix++) {
244             Node childNode = additive.jjtGetChild(ix);
245             if (childNode.jjtGetNumChildren() != 1 || childNode.hasDescendantOfType(ASTName.class)) {
246                 if (!found) {
247                     checkForViolation(rootNode, data, count);
248                     found = true;
249                 }
250                 count = 0;
251             } else {
252                 count++;
253             }
254         }
255 
256         // no variables appended, compiler will take care of merging all the
257         // string concats, we really only have 1 then
258         if (!found) {
259             count = 1;
260         }
261 
262         return count;
263     }
264 
265     /**
266      * Checks to see if there is string concatenation in the node.
267      *
268      * This method checks if it's additive with respect to the append method
269      * only.
270      *
271      * @param n
272      *            Node to check
273      * @return true if the node has an additive expression (i.e. "Hello " +
274      *         Const.WORLD)
275      */
276     private boolean isAdditive(Node n) {
277         List<ASTAdditiveExpression> lstAdditive = n.findDescendantsOfType(ASTAdditiveExpression.class);
278         if (lstAdditive.isEmpty()) {
279             return false;
280         }
281         // if there are more than 1 set of arguments above us we're not in the
282         // append
283         // but a sub-method call
284         for (int ix = 0; ix < lstAdditive.size(); ix++) {
285             ASTAdditiveExpression expr = lstAdditive.get(ix);
286             if (expr.getParentsOfType(ASTArgumentList.class).size() != 1) {
287                 return false;
288             }
289         }
290         return true;
291     }
292 
293     /**
294      * Get the first parent. Keep track of the last node though. For If
295      * statements it's the only way we can differentiate between if's and else's
296      * For switches it's the only way we can differentiate between switches
297      *
298      * @param node
299      *            The node to check
300      * @return The first parent block
301      */
302     private Node getFirstParentBlock(Node node) {
303         Node parentNode = node.jjtGetParent();
304 
305         Node lastNode = node;
306         while (parentNode != null && !BLOCK_PARENTS.contains(parentNode.getClass())) {
307             lastNode = parentNode;
308             parentNode = parentNode.jjtGetParent();
309         }
310         if (parentNode instanceof ASTIfStatement) {
311             parentNode = lastNode;
312         } else if (parentNode instanceof ASTSwitchStatement) {
313             parentNode = getSwitchParent(parentNode, lastNode);
314         }
315         return parentNode;
316     }
317 
318     /**
319      * Determine which SwitchLabel we belong to inside a switch
320      *
321      * @param parentNode
322      *            The parent node we're looking at
323      * @param lastNode
324      *            The last node processed
325      * @return The parent node for the switch statement
326      */
327     private Node getSwitchParent(Node parentNode, Node lastNode) {
328         int allChildren = parentNode.jjtGetNumChildren();
329         ASTSwitchLabel label = null;
330         for (int ix = 0; ix < allChildren; ix++) {
331             Node n = parentNode.jjtGetChild(ix);
332             if (n instanceof ASTSwitchLabel) {
333                 label = (ASTSwitchLabel) n;
334             } else if (n.equals(lastNode)) {
335                 parentNode = label;
336                 break;
337             }
338         }
339         return parentNode;
340     }
341 
342     /**
343      * Helper method checks to see if a violation occurred, and adds a
344      * RuleViolation if it did
345      */
346     private void checkForViolation(Node node, Object data, int concurrentCount) {
347         if (concurrentCount > threshold) {
348             String[] param = { String.valueOf(concurrentCount) };
349             addViolation(data, node, param);
350         }
351     }
352 
353     private boolean isAppendingStringLiteral(Node node) {
354         Node n = node;
355         while (n.jjtGetNumChildren() != 0 && !(n instanceof ASTLiteral)) {
356             n = n.jjtGetChild(0);
357         }
358         return n instanceof ASTLiteral;
359     }
360 
361     private static boolean isStringBuffer(ASTVariableDeclaratorId node) {
362 
363         if (node.getType() != null) {
364             // return node.getType().equals(StringBuffer.class);
365             return TypeHelper.isEither(node, StringBuffer.class, StringBuilder.class);
366         }
367         Node nn = node.getTypeNameNode();
368         if (nn == null || nn.jjtGetNumChildren() == 0) {
369             return false;
370         }
371         return TypeHelper.isEither((TypeNode) nn.jjtGetChild(0), StringBuffer.class, StringBuilder.class);
372     }
373 }