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