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.NameOccurrence;
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  
33  /**
34   * This rule finds concurrent calls to StringBuffer/Builder.append where String literals
35   * are used It would be much better to make these calls using one call to
36   * .append
37   * <p/>
38   * example:
39   * <p/>
40   * <pre>
41   * StringBuilder buf = new StringBuilder();
42   * buf.append(&quot;Hello&quot;);
43   * buf.append(&quot; &quot;).append(&quot;World&quot;);
44   * </pre>
45   * <p/>
46   * This would be more eloquently put as:
47   * <p/>
48   * <pre>
49   * StringBuilder buf = new StringBuilder();
50   * buf.append(&quot;Hello World&quot;);
51   * </pre>
52   * <p/>
53   * The rule takes one parameter, threshold, which defines the lower limit of
54   * consecutive appends before a violation is created. The default is 1.
55   */
56  public class ConsecutiveLiteralAppendsRule extends AbstractJavaRule {
57  
58  	private final static Set<Class<?>> BLOCK_PARENTS;
59  
60  	static {
61  		BLOCK_PARENTS = new HashSet<Class<?>>();
62  		BLOCK_PARENTS.add(ASTForStatement.class);
63  		BLOCK_PARENTS.add(ASTWhileStatement.class);
64  		BLOCK_PARENTS.add(ASTDoStatement.class);
65  		BLOCK_PARENTS.add(ASTIfStatement.class);
66  		BLOCK_PARENTS.add(ASTSwitchStatement.class);
67  		BLOCK_PARENTS.add(ASTMethodDeclaration.class);
68  	}
69  
70  	private static final IntegerProperty THRESHOLD_DESCRIPTOR = new IntegerProperty("threshold", "Max consecutive appends", 1, 10, 1, 1.0f);
71  
72  	private int threshold = 1;
73  
74  	public ConsecutiveLiteralAppendsRule() {
75  		definePropertyDescriptor(THRESHOLD_DESCRIPTOR);
76  	}
77  
78  	@Override
79  	public Object visit(ASTVariableDeclaratorId node, Object data) {
80  
81  		if (!isStringBuffer(node)) {
82  			return data;
83  		}
84  		threshold = getProperty(THRESHOLD_DESCRIPTOR);
85  
86  		int concurrentCount = checkConstructor(node, data);
87  		Node lastBlock = getFirstParentBlock(node);
88  		Node currentBlock = lastBlock;
89  		Map<VariableNameDeclaration, List<NameOccurrence>> decls = node.getScope().getVariableDeclarations();
90  		Node rootNode = null;
91  		// only want the constructor flagged if it's really containing strings
92  		if (concurrentCount >= 1) {
93  			rootNode = node;
94  		}
95  		for (Map.Entry<VariableNameDeclaration, List<NameOccurrence>> entry : decls.entrySet()) {
96  			List<NameOccurrence> decl = entry.getValue();
97  			for (NameOccurrence no : decl) {
98  				Node n = no.getLocation();
99  
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() != 1 || 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 (n instanceof ASTSwitchLabel) {
264 				label = (ASTSwitchLabel) n;
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 occurred, 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() != 0 && !(n instanceof ASTLiteral)) {
287 			n = n.jjtGetChild(0);
288 		}
289 		return n 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 			return TypeHelper.isEither(node, StringBuffer.class, StringBuilder.class);
297 		}
298 		Node nn = node.getTypeNameNode();
299 		if (nn.jjtGetNumChildren() == 0) {
300 			return false;
301 		}
302 		return TypeHelper.isEither((TypeNode) nn.jjtGetChild(0), StringBuffer.class, StringBuilder.class);
303 	}
304 }