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 }