/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.lang.java.rule.strings;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.sourceforge.pmd.lang.ast.Node;
import net.sourceforge.pmd.lang.java.ast.ASTAdditiveExpression;
import net.sourceforge.pmd.lang.java.ast.ASTAllocationExpression;
import net.sourceforge.pmd.lang.java.ast.ASTArgumentList;
import net.sourceforge.pmd.lang.java.ast.ASTDoStatement;
import net.sourceforge.pmd.lang.java.ast.ASTForStatement;
import net.sourceforge.pmd.lang.java.ast.ASTIfStatement;
import net.sourceforge.pmd.lang.java.ast.ASTLiteral;
import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTName;
import net.sourceforge.pmd.lang.java.ast.ASTPrimaryExpression;
import net.sourceforge.pmd.lang.java.ast.ASTPrimarySuffix;
import net.sourceforge.pmd.lang.java.ast.ASTSwitchLabel;
import net.sourceforge.pmd.lang.java.ast.ASTSwitchStatement;
import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclaratorId;
import net.sourceforge.pmd.lang.java.ast.ASTVariableInitializer;
import net.sourceforge.pmd.lang.java.ast.ASTWhileStatement;
import net.sourceforge.pmd.lang.java.ast.TypeNode;
import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
import net.sourceforge.pmd.lang.java.symboltable.JavaNameOccurrence;
import net.sourceforge.pmd.lang.java.symboltable.VariableNameDeclaration;
import net.sourceforge.pmd.lang.java.typeresolution.TypeHelper;
import net.sourceforge.pmd.lang.rule.properties.IntegerProperty;
import net.sourceforge.pmd.lang.symboltable.NameOccurrence;
/**
* This rule finds concurrent calls to StringBuffer/Builder.append where String
* literals are used It would be much better to make these calls using one call
* to <code>.append</code>
*
* <p>Example:</p>
*
* <pre>
* StringBuilder buf = new StringBuilder();
* buf.append("Hello");
* buf.append(" ").append("World");
* </pre>
*
* <p>This would be more eloquently put as:</p>
*
* <pre>
* StringBuilder buf = new StringBuilder();
* buf.append("Hello World");
* </pre>
*
* <p>The rule takes one parameter, threshold, which defines the lower limit of
* consecutive appends before a violation is created. The default is 1.</p>
*/
public class ConsecutiveLiteralAppendsRule extends AbstractJavaRule {
private static final Set<Class<?>> BLOCK_PARENTS;
static {
BLOCK_PARENTS = new HashSet<>();
BLOCK_PARENTS.add(ASTForStatement.class);
BLOCK_PARENTS.add(ASTWhileStatement.class);
BLOCK_PARENTS.add(ASTDoStatement.class);
BLOCK_PARENTS.add(ASTIfStatement.class);
BLOCK_PARENTS.add(ASTSwitchStatement.class);
BLOCK_PARENTS.add(ASTMethodDeclaration.class);
}
private static final IntegerProperty THRESHOLD_DESCRIPTOR = new IntegerProperty("threshold",
"Max consecutive appends", 1, 10, 1, 1.0f);
private int threshold = 1;
public ConsecutiveLiteralAppendsRule() {
definePropertyDescriptor(THRESHOLD_DESCRIPTOR);
}
@Override
public Object visit(ASTVariableDeclaratorId node, Object data) {
if (!isStringBuffer(node)) {
return data;
}
threshold = getProperty(THRESHOLD_DESCRIPTOR);
int concurrentCount = checkConstructor(node, data);
if (hasInitializer(node)) {
concurrentCount += checkInitializerExpressions(node);
}
Node lastBlock = getFirstParentBlock(node);
Node currentBlock = lastBlock;
Map<VariableNameDeclaration, List<NameOccurrence>> decls = node.getScope()
.getDeclarations(VariableNameDeclaration.class);
Node rootNode = null;
// only want the constructor flagged if it's really containing strings
if (concurrentCount >= 1) {
rootNode = node;
}
for (List<NameOccurrence> decl : decls.values()) {
for (NameOccurrence no : decl) {
JavaNameOccurrence jno = (JavaNameOccurrence) no;
Node n = jno.getLocation();
// skip the declarations/usages, that deal with a different
// variable
if (!node.getImage().equals(jno.getImage())) {
continue;
}
currentBlock = getFirstParentBlock(n);
if (!InefficientStringBufferingRule.isInStringBufferOperation(n, 3, "append")) {
if (!jno.isPartOfQualifiedName()) {
checkForViolation(rootNode, data, concurrentCount);
concurrentCount = 0;
}
continue;
}
ASTPrimaryExpression s = n.getFirstParentOfType(ASTPrimaryExpression.class);
int numChildren = s.jjtGetNumChildren();
for (int jx = 0; jx < numChildren; jx++) {
Node sn = s.jjtGetChild(jx);
if (!(sn instanceof ASTPrimarySuffix) || sn.getImage() != null) {
continue;
}
// see if it changed blocks
if (currentBlock != null && lastBlock != null && !currentBlock.equals(lastBlock)
|| currentBlock == null ^ lastBlock == null) {
checkForViolation(rootNode, data, concurrentCount);
concurrentCount = 0;
}
// if concurrent is 0 then we reset the root to report from
// here
if (concurrentCount == 0) {
rootNode = sn;
}
if (isAdditive(sn)) {
concurrentCount = processAdditive(data, concurrentCount, sn, rootNode);
if (concurrentCount != 0) {
rootNode = sn;
}
} else if (!isAppendingStringLiteral(sn)) {
checkForViolation(rootNode, data, concurrentCount);
concurrentCount = 0;
} else {
concurrentCount++;
}
lastBlock = currentBlock;
}
}
}
checkForViolation(rootNode, data, concurrentCount);
return data;
}
/**
* Determine if the constructor contains (or ends with) a String Literal
*
* @param node
* @return 1 if the constructor contains string argument, else 0
*/
private int checkConstructor(ASTVariableDeclaratorId node, Object data) {
Node parent = node.jjtGetParent();
if (parent.jjtGetNumChildren() >= 2) {
ASTAllocationExpression allocationExpression = parent.jjtGetChild(1)
.getFirstDescendantOfType(ASTAllocationExpression.class);
ASTArgumentList list = null;
if (allocationExpression != null) {
list = allocationExpression.getFirstDescendantOfType(ASTArgumentList.class);
}
if (list != null) {
ASTLiteral literal = list.getFirstDescendantOfType(ASTLiteral.class);
if (!isAdditive(list) && literal != null && literal.isStringLiteral()) {
return 1;
}
return processAdditive(data, 0, list, node);
}
}
return 0;
}
/**
* Determine if during the variable initializer calls to ".append" are done.
*
* @param node
* @return
*/
private int checkInitializerExpressions(ASTVariableDeclaratorId node) {
ASTVariableInitializer initializer = node.jjtGetParent().getFirstChildOfType(ASTVariableInitializer.class);
ASTPrimaryExpression primary = initializer.getFirstDescendantOfType(ASTPrimaryExpression.class);
int result = 0;
boolean previousWasAppend = false;
for (int i = 0; i < primary.jjtGetNumChildren(); i++) {
Node child = primary.jjtGetChild(i);
if (child.jjtGetNumChildren() > 0 && child.jjtGetChild(0) instanceof ASTAllocationExpression) {
// skip the constructor call, that has already been checked
continue;
}
if (child instanceof ASTPrimarySuffix) {
ASTPrimarySuffix suffix = (ASTPrimarySuffix) child;
if (suffix.jjtGetNumChildren() == 0 && suffix.hasImageEqualTo("append")) {
previousWasAppend = true;
} else if (suffix.jjtGetNumChildren() > 0 && previousWasAppend) {
previousWasAppend = false;
ASTLiteral literal = suffix.getFirstDescendantOfType(ASTLiteral.class);
if (literal != null && literal.isStringLiteral()) {
result++;
} else {
// if it was not a String literal that was appended,
// then we don't
// have a consecutive literal string append anymore and
// we can skip
// checking the remainder of the initializer
break;
}
}
}
}
return result;
}
private boolean hasInitializer(ASTVariableDeclaratorId node) {
return node.jjtGetParent().hasDescendantOfType(ASTVariableInitializer.class);
}
private int processAdditive(Object data, int concurrentCount, Node sn, Node rootNode) {
ASTAdditiveExpression additive = sn.getFirstDescendantOfType(ASTAdditiveExpression.class);
// The additive expression must of be type String to count
if (additive == null || additive.getType() != null && !TypeHelper.isA(additive, String.class)) {
return 0;
}
// check for at least one string literal
List<ASTLiteral> literals = additive.findDescendantsOfType(ASTLiteral.class);
boolean stringLiteralFound = false;
for (ASTLiteral l : literals) {
if (l.isCharLiteral() || l.isStringLiteral()) {
stringLiteralFound = true;
break;
}
}
if (!stringLiteralFound) {
return 0;
}
int count = concurrentCount;
boolean found = false;
for (int ix = 0; ix < additive.jjtGetNumChildren(); ix++) {
Node childNode = additive.jjtGetChild(ix);
if (childNode.jjtGetNumChildren() != 1 || childNode.hasDescendantOfType(ASTName.class)) {
if (!found) {
checkForViolation(rootNode, data, count);
found = true;
}
count = 0;
} else {
count++;
}
}
// no variables appended, compiler will take care of merging all the
// string concats, we really only have 1 then
if (!found) {
count = 1;
}
return count;
}
/**
* Checks to see if there is string concatenation in the node.
*
* This method checks if it's additive with respect to the append method
* only.
*
* @param n
* Node to check
* @return true if the node has an additive expression (i.e. "Hello " +
* Const.WORLD)
*/
private boolean isAdditive(Node n) {
List<ASTAdditiveExpression> lstAdditive = n.findDescendantsOfType(ASTAdditiveExpression.class);
if (lstAdditive.isEmpty()) {
return false;
}
// if there are more than 1 set of arguments above us we're not in the
// append
// but a sub-method call
for (int ix = 0; ix < lstAdditive.size(); ix++) {
ASTAdditiveExpression expr = lstAdditive.get(ix);
if (expr.getParentsOfType(ASTArgumentList.class).size() != 1) {
return false;
}
}
return true;
}
/**
* Get the first parent. Keep track of the last node though. For If
* statements it's the only way we can differentiate between if's and else's
* For switches it's the only way we can differentiate between switches
*
* @param node
* The node to check
* @return The first parent block
*/
private Node getFirstParentBlock(Node node) {
Node parentNode = node.jjtGetParent();
Node lastNode = node;
while (parentNode != null && !BLOCK_PARENTS.contains(parentNode.getClass())) {
lastNode = parentNode;
parentNode = parentNode.jjtGetParent();
}
if (parentNode instanceof ASTIfStatement) {
parentNode = lastNode;
} else if (parentNode instanceof ASTSwitchStatement) {
parentNode = getSwitchParent(parentNode, lastNode);
}
return parentNode;
}
/**
* Determine which SwitchLabel we belong to inside a switch
*
* @param parentNode
* The parent node we're looking at
* @param lastNode
* The last node processed
* @return The parent node for the switch statement
*/
private Node getSwitchParent(Node parentNode, Node lastNode) {
int allChildren = parentNode.jjtGetNumChildren();
Node result = parentNode;
ASTSwitchLabel label = null;
for (int ix = 0; ix < allChildren; ix++) {
Node n = result.jjtGetChild(ix);
if (n instanceof ASTSwitchLabel) {
label = (ASTSwitchLabel) n;
} else if (n.equals(lastNode)) {
result = label;
break;
}
}
return result;
}
/**
* Helper method checks to see if a violation occurred, and adds a
* RuleViolation if it did
*/
private void checkForViolation(Node node, Object data, int concurrentCount) {
if (concurrentCount > threshold) {
String[] param = { String.valueOf(concurrentCount) };
addViolation(data, node, param);
}
}
private boolean isAppendingStringLiteral(Node node) {
Node n = node;
while (n.jjtGetNumChildren() != 0 && !(n instanceof ASTLiteral)) {
n = n.jjtGetChild(0);
}
return n instanceof ASTLiteral;
}
private static boolean isStringBuffer(ASTVariableDeclaratorId node) {
if (node.getType() != null) {
// return node.getType().equals(StringBuffer.class);
return TypeHelper.isEither(node, StringBuffer.class, StringBuilder.class);
}
Node nn = node.getTypeNameNode();
if (nn == null || nn.jjtGetNumChildren() == 0) {
return false;
}
return TypeHelper.isEither((TypeNode) nn.jjtGetChild(0), StringBuffer.class, StringBuilder.class);
}
}