/**
* 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.Set;
import net.sourceforge.pmd.lang.ast.Node;
import net.sourceforge.pmd.lang.java.ast.ASTArgumentList;
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.rule.AbstractJavaRule;
import net.sourceforge.pmd.lang.java.symboltable.TypedNameDeclaration;
import net.sourceforge.pmd.lang.java.typeresolution.TypeHelper;
import net.sourceforge.pmd.lang.symboltable.NameDeclaration;
/**
* This rule finds places where StringBuffer.toString() is called just to see if
* the string is 0 length by either using .equals("") or toString().length().
*
* <pre>
* StringBuffer sb = new StringBuffer("some string");
* if (sb.toString().equals("")) {
* // this is wrong
* }
* if (sb.length() == 0) {
* // this is right
* }
* </pre>
*
* @author acaplan
* @author Philip Graf
*/
public class UseStringBufferLengthRule extends AbstractJavaRule {
// FIXME Need to remove this somehow.
/*
* Specifically, we need a symbol tree that can be traversed downwards, so
* that instead of visiting each name and then visiting the declaration for
* that name, we should visit all the declarations and check their usages.
* With that in place, this rule would be reduced to: - find all
* StringBuffer declarations - check each usage - flag those that involve
* variable.toString()
*/
private Set<NameDeclaration> alreadySeen = new HashSet<>();
@Override
public Object visit(ASTMethodDeclaration acu, Object data) {
alreadySeen.clear();
return super.visit(acu, data);
}
@Override
public Object visit(ASTName decl, Object data) {
if (!decl.getImage().endsWith("toString")) {
return data;
}
NameDeclaration nd = decl.getNameDeclaration();
if (nd == null) {
return data;
}
if (alreadySeen.contains(nd) || !(nd instanceof TypedNameDeclaration) || nd instanceof TypedNameDeclaration
&& TypeHelper.isNeither((TypedNameDeclaration) nd, StringBuffer.class, StringBuilder.class)) {
return data;
}
alreadySeen.add(nd);
if (isViolation(decl)) {
addViolation(data, decl);
}
return data;
}
/**
* Returns true for the following violations:
*
* <pre>
* StringBuffer sb = new StringBuffer("some string");
* if (sb.toString().equals("")) {
* // this is a violation
* }
* if (sb.toString().length() == 0) {
* // this is a violation
* }
* if (sb.length() == 0) {
* // this is ok
* }
* </pre>
*/
private boolean isViolation(ASTName decl) {
// the (grand)parent of a violation has four children
Node parent = decl.jjtGetParent().jjtGetParent();
if (parent.jjtGetNumChildren() == 4) {
// 1. child: sb.toString where sb is a VariableNameDeclaration for a
// StringBuffer or StringBuilder
if (parent.jjtGetChild(0).getFirstChildOfType(ASTName.class).getImage().endsWith(".toString")) {
// 2. child: the arguments of toString
// no need to check as both StringBuffer and StringBuilder only
// have one toString method
// 3. child: equals or length, 4. child: their arguments
return isEqualsViolation(parent) || isLengthViolation(parent);
}
}
return false;
}
private boolean isEqualsViolation(Node parent) {
// 3. child: equals
if (parent.jjtGetChild(2).hasImageEqualTo("equals")) {
// 4. child: the arguments of equals, there must be exactly one and
// it must be ""
List<ASTArgumentList> argList = parent.jjtGetChild(3).findDescendantsOfType(ASTArgumentList.class);
if (argList.size() == 1) {
List<ASTLiteral> literals = argList.get(0).findDescendantsOfType(ASTLiteral.class);
return literals.size() == 1 && literals.get(0).hasImageEqualTo("\"\"");
}
}
return false;
}
private boolean isLengthViolation(Node parent) {
// 3. child: length
return parent.jjtGetChild(2).hasImageEqualTo("length");
// 4. child: the arguments of length
// no need to check as String has only one length method
}
}