/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.lang.java.rule.strings;
import java.util.HashMap;
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.ASTBlockStatement;
import net.sourceforge.pmd.lang.java.ast.ASTCastExpression;
import net.sourceforge.pmd.lang.java.ast.ASTFieldDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTFormalParameter;
import net.sourceforge.pmd.lang.java.ast.ASTIfStatement;
import net.sourceforge.pmd.lang.java.ast.ASTLiteral;
import net.sourceforge.pmd.lang.java.ast.ASTMultiplicativeExpression;
import net.sourceforge.pmd.lang.java.ast.ASTName;
import net.sourceforge.pmd.lang.java.ast.ASTPrimaryExpression;
import net.sourceforge.pmd.lang.java.ast.ASTPrimaryPrefix;
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.ASTVariableDeclarator;
import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclaratorId;
import net.sourceforge.pmd.lang.java.ast.ASTVariableInitializer;
import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
import net.sourceforge.pmd.lang.java.symboltable.JavaNameOccurrence;
import net.sourceforge.pmd.lang.java.typeresolution.TypeHelper;
import net.sourceforge.pmd.lang.symboltable.NameOccurrence;
/**
* This rule finds StringBuffers which may have been pre-sized incorrectly.
*
* @author Allan Caplan
* @see <a href="https://sourceforge.net/p/pmd/discussion/188194/thread/aba9dae7/">Check StringBuffer sizes against usage </a>
*/
public class InsufficientStringBufferDeclarationRule extends AbstractJavaRule {
private static final Set<Class<? extends Node>> BLOCK_PARENTS;
static {
BLOCK_PARENTS = new HashSet<>(2);
BLOCK_PARENTS.add(ASTIfStatement.class);
BLOCK_PARENTS.add(ASTSwitchStatement.class);
}
// as specified in StringBuffer and StringBuilder
public static final int DEFAULT_BUFFER_SIZE = 16;
@Override
public Object visit(ASTVariableDeclaratorId node, Object data) {
if (!TypeHelper.isEither(node.getNameDeclaration(), StringBuffer.class, StringBuilder.class)) {
return data;
}
Node rootNode = node;
int anticipatedLength = 0;
int constructorLength = DEFAULT_BUFFER_SIZE;
constructorLength = getConstructorLength(node, constructorLength);
anticipatedLength = getInitialLength(node);
anticipatedLength += getConstructorAppendsLength(node);
List<NameOccurrence> usage = node.getUsages();
Map<Node, Map<Node, Integer>> blocks = new HashMap<>();
for (NameOccurrence no : usage) {
JavaNameOccurrence jno = (JavaNameOccurrence) no;
Node n = jno.getLocation();
if (!InefficientStringBufferingRule.isInStringBufferOperation(n, 3, "append")) {
if (!jno.isOnLeftHandSide()
&& !InefficientStringBufferingRule.isInStringBufferOperation(n, 3, "setLength")) {
continue;
}
if (constructorLength != -1 && anticipatedLength > constructorLength) {
anticipatedLength += processBlocks(blocks);
String[] param = { String.valueOf(constructorLength), String.valueOf(anticipatedLength) };
addViolation(data, rootNode, param);
}
constructorLength = getConstructorLength(n, constructorLength);
rootNode = n;
anticipatedLength = getInitialLength(node);
}
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;
}
int thisSize = 0;
Node block = getFirstParentBlock(sn);
if (isAdditive(sn)) {
thisSize = processAdditive(sn);
} else {
thisSize = processNode(sn);
}
if (block != null) {
storeBlockStatistics(blocks, thisSize, block);
} else {
anticipatedLength += thisSize;
}
}
}
anticipatedLength += processBlocks(blocks);
if (constructorLength != -1 && anticipatedLength > constructorLength) {
String[] param = { String.valueOf(constructorLength), String.valueOf(anticipatedLength) };
addViolation(data, rootNode, param);
}
return data;
}
/**
* This rule is concerned with IF and Switch blocks. Process the block into
* a local Map, from which we can later determine which is the longest block
* inside
*
* @param blocks
* The map of blocks in the method being investigated
* @param thisSize
* The size of the current block
* @param block
* The block in question
*/
private void storeBlockStatistics(Map<Node, Map<Node, Integer>> blocks, int thisSize, Node block) {
Node statement = block.jjtGetParent();
if (block.jjtGetParent() instanceof ASTIfStatement) {
// Else Ifs are their own subnode in AST. So we have to
// look a little farther up the tree to find the IF statement
Node possibleStatement = statement.getFirstParentOfType(ASTIfStatement.class);
while (possibleStatement instanceof ASTIfStatement) {
statement = possibleStatement;
possibleStatement = possibleStatement.getFirstParentOfType(ASTIfStatement.class);
}
}
Map<Node, Integer> thisBranch = blocks.get(statement);
if (thisBranch == null) {
thisBranch = new HashMap<>();
blocks.put(statement, thisBranch);
}
Integer x = thisBranch.get(block);
if (x != null) {
thisSize += x;
}
thisBranch.put(statement, thisSize);
}
private int processBlocks(Map<Node, Map<Node, Integer>> blocks) {
int anticipatedLength = 0;
int ifLength = 0;
for (Map.Entry<Node, Map<Node, Integer>> entry : blocks.entrySet()) {
ifLength = 0;
for (Map.Entry<Node, Integer> entry2 : entry.getValue().entrySet()) {
Integer value = entry2.getValue();
ifLength = Math.max(ifLength, value.intValue());
}
anticipatedLength += ifLength;
}
return anticipatedLength;
}
private int processAdditive(Node sn) {
ASTAdditiveExpression additive = sn.getFirstDescendantOfType(ASTAdditiveExpression.class);
if (additive == null) {
return 0;
}
int anticipatedLength = 0;
for (int ix = 0; ix < additive.jjtGetNumChildren(); ix++) {
Node childNode = additive.jjtGetChild(ix);
ASTLiteral literal = childNode.getFirstDescendantOfType(ASTLiteral.class);
if (literal != null && literal.getImage() != null) {
anticipatedLength += literal.getImage().length() - 2;
}
}
return anticipatedLength;
}
private static boolean isStringOrCharLiteral(ASTLiteral literal) {
return literal.isStringLiteral() || literal.isCharLiteral();
}
private int processNode(Node sn) {
int anticipatedLength = 0;
if (sn != null) {
ASTPrimaryPrefix xn = sn.getFirstDescendantOfType(ASTPrimaryPrefix.class);
if (xn != null) {
if (xn.jjtGetNumChildren() != 0 && xn.jjtGetChild(0) instanceof ASTLiteral) {
ASTLiteral literal = (ASTLiteral) xn.jjtGetChild(0);
String str = xn.jjtGetChild(0).getImage();
if (str != null) {
if (literal.isStringLiteral()) {
anticipatedLength += str.length() - 2;
} else if (literal.isCharLiteral()) {
anticipatedLength += 1;
} else if (literal.isIntLiteral() && str.startsWith("0x")) {
// but only if we are not inside a cast expression
Node parentNode = literal.jjtGetParent().jjtGetParent().jjtGetParent();
if (parentNode instanceof ASTCastExpression
&& ((ASTCastExpression) parentNode).getType() == char.class) {
anticipatedLength += 1;
} else {
// e.g. 0xdeadbeef -> will be converted to a
// base 10 integer string: 3735928559
anticipatedLength += String.valueOf(Long.parseLong(str.substring(2), 16)).length();
}
} else {
anticipatedLength += str.length();
}
}
}
}
}
return anticipatedLength;
}
private int getConstructorLength(Node node, int constructorLength) {
int iConstructorLength = constructorLength;
Node block = node.getFirstParentOfType(ASTBlockStatement.class);
if (block == null) {
block = node.getFirstParentOfType(ASTFieldDeclaration.class);
}
if (block == null) {
block = node.getFirstParentOfType(ASTFormalParameter.class);
if (block != null) {
iConstructorLength = -1;
} else {
return DEFAULT_BUFFER_SIZE;
}
}
// if there is any addition/subtraction going on then just use the
// default.
ASTAdditiveExpression exp = block.getFirstDescendantOfType(ASTAdditiveExpression.class);
if (exp != null) {
return DEFAULT_BUFFER_SIZE;
}
ASTMultiplicativeExpression mult = block.getFirstDescendantOfType(ASTMultiplicativeExpression.class);
if (mult != null) {
return DEFAULT_BUFFER_SIZE;
}
List<ASTLiteral> literals;
ASTAllocationExpression constructorCall = block.getFirstDescendantOfType(ASTAllocationExpression.class);
if (constructorCall != null) {
// if this is a constructor call, only consider the literals within
// it.
literals = constructorCall.findDescendantsOfType(ASTLiteral.class);
} else {
// otherwise it might be a setLength call...
literals = block.findDescendantsOfType(ASTLiteral.class);
}
if (literals.isEmpty()) {
List<ASTName> name = block.findDescendantsOfType(ASTName.class);
if (!name.isEmpty()) {
iConstructorLength = -1;
}
} else if (literals.size() == 1) {
ASTLiteral literal = literals.get(0);
String str = literal.getImage();
if (str == null) {
iConstructorLength = 0;
} else if (isStringOrCharLiteral(literal)) {
// since it's not taken into account
// anywhere. only count the extra 16
// characters
// don't add the constructor's length
iConstructorLength = 14 + str.length();
} else if (literal.isIntLiteral() && str.startsWith("0x")) {
// bug 3516101 - the string could be a hex number
iConstructorLength = Integer.parseInt(str.substring(2), 16);
} else {
iConstructorLength = Integer.parseInt(str);
}
} else {
iConstructorLength = -1;
}
if (iConstructorLength == 0) {
if (constructorLength == -1) {
iConstructorLength = DEFAULT_BUFFER_SIZE;
} else {
iConstructorLength = constructorLength;
}
}
return iConstructorLength;
}
private int getInitialLength(Node node) {
Node block = node.getFirstParentOfType(ASTBlockStatement.class);
if (block == null) {
block = node.getFirstParentOfType(ASTFieldDeclaration.class);
if (block == null) {
block = node.getFirstParentOfType(ASTFormalParameter.class);
}
}
List<ASTLiteral> literals = block.findDescendantsOfType(ASTLiteral.class);
if (literals.size() == 1) {
ASTLiteral literal = literals.get(0);
String str = literal.getImage();
if (str != null && isStringOrCharLiteral(literal)) {
return str.length() - 2; // take off the quotes
}
}
return 0;
}
private int getConstructorAppendsLength(final Node node) {
final Node parent = node.getFirstParentOfType(ASTVariableDeclarator.class);
int size = 0;
if (parent != null) {
final Node initializer = parent.getFirstChildOfType(ASTVariableInitializer.class);
if (initializer != null) {
final Node primExp = initializer.getFirstDescendantOfType(ASTPrimaryExpression.class);
if (primExp != null) {
for (int i = 0; i < primExp.jjtGetNumChildren(); i++) {
final Node sn = primExp.jjtGetChild(i);
if (!(sn instanceof ASTPrimarySuffix) || sn.getImage() != null) {
continue;
}
size += processNode(sn);
}
}
}
}
return size;
}
private boolean isAdditive(Node n) {
ASTAdditiveExpression add = n.getFirstDescendantOfType(ASTAdditiveExpression.class);
// if the first descendant additive expression is deeper than 4 levels,
// it belongs to a nested method call and not anymore to the append
// argument.
return add != null && add.getNthParent(4) == n;
}
/**
* Locate the block that the given node is in, if any
*
* @param node
* The node we're looking for a parent of
* @return Node - The node that corresponds to any block that may be a
* parent of this object
*/
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 static Node getSwitchParent(Node parentNode, Node lastNode) {
int allChildren = parentNode.jjtGetNumChildren();
ASTSwitchLabel label = null;
for (int ix = 0; ix < allChildren; ix++) {
Node n = parentNode.jjtGetChild(ix);
if (n instanceof ASTSwitchLabel) {
label = (ASTSwitchLabel) n;
} else if (n.equals(lastNode)) {
parentNode = label;
break;
}
}
return parentNode;
}
}