package quickfix;
import static edu.umd.cs.findbugs.plugin.eclipse.quickfix.util.ASTUtil.addImports;
import static edu.umd.cs.findbugs.plugin.eclipse.quickfix.util.ASTUtil.getASTNode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.Nonnull;
import com.mebigfatguy.fbcontrib.detect.CharsetIssues;
import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.plugin.eclipse.quickfix.BugResolution;
import edu.umd.cs.findbugs.plugin.eclipse.quickfix.CustomLabelVisitor;
import edu.umd.cs.findbugs.plugin.eclipse.quickfix.exception.BugResolutionException;
import org.apache.bcel.generic.Type;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import util.QMethodAndArgs;
public class CharsetIssuesResolution extends BugResolution {
private boolean isName;
@Override
protected boolean resolveBindings() {
return true;
}
@Override
public void setOptions(@Nonnull Map<String, String> options) {
isName = Boolean.parseBoolean(options.get("isName"));
}
@Override
protected ASTVisitor getCustomLabelVisitor() {
return new CSIVisitorAndFixer();
}
@Override
protected void repairBug(ASTRewrite rewrite, CompilationUnit workingUnit, BugInstance bug) throws BugResolutionException {
ASTNode node = getASTNode(workingUnit, bug.getPrimarySourceLineAnnotation());
CSIVisitorAndFixer visitor = new CSIVisitorAndFixer(rewrite);
node.accept(visitor);
ASTNode badUseOfLiteral = visitor.getBadInvocation();
ASTNode fixedUseOfStandardCharset = visitor.getFixedInvocation();
rewrite.replace(badUseOfLiteral, fixedUseOfStandardCharset, null);
addImports(rewrite, workingUnit, "java.nio.charset.StandardCharsets");
}
private final class CSIVisitorAndFixer extends ASTVisitor implements CustomLabelVisitor {
private Map<QMethodAndArgs, Object> csiConstructors;
private Map<QMethodAndArgs, Object> csiMethods;
private ASTNode fixedAstNode = null;
private AST rootAstNode;
private ASTRewrite rewrite;
private ASTNode badConstructorInvocation;
private ASTNode badMethodInvocation;
private String literalValue = null;
public CSIVisitorAndFixer(ASTRewrite rewrite) {
this();
this.rootAstNode = rewrite.getAST();
this.rewrite = rewrite;
}
public CSIVisitorAndFixer() { // for label traversing
if (isName) {
parseToTypeArgs(CharsetIssues.getUnreplaceableCharsetEncodings());
} else {
parseToTypeArgs(CharsetIssues.getReplaceableCharsetEncodings());
}
}
public ASTNode getFixedInvocation() {
return fixedAstNode;
}
public ASTNode getBadInvocation() {
if (badConstructorInvocation != null) {
return badConstructorInvocation;
}
return badMethodInvocation;
}
private void parseToTypeArgs(Map<String, ? extends Object> map) {
this.csiConstructors = new HashMap<>();
this.csiMethods = new HashMap<>();
for (Entry<String, ? extends Object> entry : map.entrySet()) {
QMethodAndArgs struct = make(entry.getKey());
if (QMethodAndArgs.CONSTRUCTOR_METHOD.equals(struct.invokedMethodString)) {
csiConstructors.put(struct, entry.getValue());
}
else {
csiMethods.put(struct, entry.getValue());
}
}
}
@SuppressWarnings("unchecked")
@Override
public boolean visit(MethodInvocation node) {
if (foundThingToReplace()) {
return false;
}
QMethodAndArgs key = QMethodAndArgs.make(node);
if (csiMethods.containsKey(key)) {
List<Expression> arguments = node.arguments();
Integer indexVal = (Integer) csiMethods.get(key);
// converts from ith from the end to nth (from the beginning)
int indexOfArgumentToReplace = (arguments.size() - indexVal) - 1;
// if this was a constant string, resolveConstantExpressionValue() will be nonnull
Object literalString = arguments.get(indexOfArgumentToReplace).resolveConstantExpressionValue();
if (null != literalString) {
this.literalValue = literalString.toString();
this.badMethodInvocation = node;
fixedAstNode = makeFixedMethodInvocation(node, indexOfArgumentToReplace);
return false; // don't keep parsing
}
}
return true;
}
@SuppressWarnings("unchecked")
private MethodInvocation makeFixedMethodInvocation(MethodInvocation node, int indexOfArgumentToReplace) {
if (rootAstNode == null || rewrite == null) {
return null;
}
MethodInvocation newNode = rootAstNode.newMethodInvocation();
newNode.setExpression((Expression) rewrite.createCopyTarget(node.getExpression()));
newNode.setName(rootAstNode.newSimpleName(node.getName().getIdentifier()));
List<Expression> newArgs = newNode.arguments();
List<Expression> oldArgs = node.arguments();
copyArgsAndReplaceWithCharset(oldArgs, newArgs, indexOfArgumentToReplace);
return newNode;
}
@SuppressWarnings("unchecked")
@Override
public boolean visit(ClassInstanceCreation node) {
if (foundThingToReplace()) {
return false;
}
QMethodAndArgs key = QMethodAndArgs.make(node);
if (csiConstructors.containsKey(key)) {
List<Expression> arguments = node.arguments();
Integer indexVal = (Integer) csiConstructors.get(key);
int indexOfArgumentToReplace = (arguments.size() - indexVal) - 1;
// if this was a constant string, resolveConstantExpressionValue() will be nonnull
Object literalString = arguments.get(indexOfArgumentToReplace).resolveConstantExpressionValue();
if (null != literalString) {
this.literalValue = literalString.toString();
this.badConstructorInvocation = node;
fixedAstNode = makeFixedConstructorInvocation(node, indexOfArgumentToReplace);
return false; // don't keep parsing
}
}
return true;
}
@SuppressWarnings("unchecked")
private ASTNode makeFixedConstructorInvocation(ClassInstanceCreation node, int indexOfArgumentToReplace) {
if (rootAstNode == null || rewrite == null) {
return null;
}
ClassInstanceCreation newNode = rootAstNode.newClassInstanceCreation();
newNode.setType((org.eclipse.jdt.core.dom.Type) rewrite.createCopyTarget(node.getType()));
List<Expression> newArgs = newNode.arguments();
List<Expression> oldArgs = node.arguments();
copyArgsAndReplaceWithCharset(oldArgs, newArgs, indexOfArgumentToReplace);
return newNode;
}
private void copyArgsAndReplaceWithCharset(List<Expression> oldArgs, List<Expression> newArgs,
int indexOfArgumentToReplace) {
for (int i = 0; i < oldArgs.size(); i++) {
if (i != indexOfArgumentToReplace) {
newArgs.add((Expression) rewrite.createCopyTarget(oldArgs.get(i)));
} else {
newArgs.add(makeCharsetReplacement());
}
}
}
private Expression makeCharsetReplacement() {
if (literalValue != null) {
String stringLiteral = literalValue.replace('-', '_');
QualifiedName qualifiedCharset = rootAstNode.newQualifiedName(rootAstNode.newName("StandardCharsets"),
rootAstNode.newSimpleName(stringLiteral));
if (isName) {
MethodInvocation charsetName = rootAstNode.newMethodInvocation();
charsetName.setExpression(qualifiedCharset);
charsetName.setName(rootAstNode.newSimpleName("name"));
return charsetName;
}
return qualifiedCharset;
}
throw new RuntimeException("No String literal in CSI quickfix");
}
private boolean foundThingToReplace() {
return this.fixedAstNode != null;
}
@Override
public String getLabelReplacement() {
return literalValue.replace('-', '_');
}
// expecting in form "java/io/InputStreamReader.<init>(Ljava/io/InputStream;Ljava/lang/String;)V"
private QMethodAndArgs make(String fullSignatureWithArgs) {
int firstSplitIndex = fullSignatureWithArgs.indexOf('.');
int secondSplitIndex = fullSignatureWithArgs.indexOf('(');
String qualifiedTypeWithSlashes = fullSignatureWithArgs.substring(0, firstSplitIndex);
String qtype = qualifiedTypeWithSlashes.replace('/', '.');
String method = fullSignatureWithArgs.substring(firstSplitIndex + 1, secondSplitIndex);
List<String> argumentTypes = new ArrayList<>();
Type[] bcelTypes = Type.getArgumentTypes(fullSignatureWithArgs.substring(secondSplitIndex));
for (Type t : bcelTypes) {
argumentTypes.add(t.toString()); // toString returns them in human-readable dot notation
}
return new QMethodAndArgs(qtype, method, argumentTypes);
}
}
}