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.HashSet;
import java.util.Map;
import java.util.Set;
import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import edu.umd.cs.findbugs.plugin.eclipse.quickfix.ApplicabilityVisitor;
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.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.Assignment;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IfStatement;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.PrefixExpression;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import util.QMethod;
public class ReturnValueIgnoreResolution extends BugResolution {
private final static String exceptionalSysOut = "System.out.println(\"Exceptional return value\");";
public final static String descriptionForWrapIf = "Replace with <code><pre>if (YYY) {\n\t" + exceptionalSysOut
+ "\n}</pre></code>";
public final static String descriptionForNegatedWrapIf = "Replace with <code><pre>if (!YYY) {\n\t" + exceptionalSysOut
+ "\n}</pre></code>";
public final static String descriptionForNewLocal = "Makes a new local variable and assigns the result of the method call to it.";
public final static String descriptionForStoreToSelf = "Stores the result of the method call back to the original method caller.";
private String description;
private static Set<String> immutableTypes = new HashSet<String>();
private static Set<QMethod> shouldNotBeIgnored = new HashSet<QMethod>();
static {
immutableTypes.add("java.lang.String");
immutableTypes.add("java.math.BigDecimal");
immutableTypes.add("java.math.BigInteger");
immutableTypes.add("java.sql.Connection");
immutableTypes.add("java.net.InetAddress");
immutableTypes.add("jsr166z.forkjoin.ParallelArray");
immutableTypes.add("jsr166z.forkjoin.ParallelLongArray");
immutableTypes.add("jsr166z.forkjoin.ParallelDoubleArray");
shouldNotBeIgnored.add(new QMethod("java.util.Iterator", "hasNext"));
shouldNotBeIgnored.add(new QMethod("java.security.MessageDigest", "digest"));
shouldNotBeIgnored.add(new QMethod("java.util.concurrent.locks.ReadWriteLock", "readLock"));
shouldNotBeIgnored.add(new QMethod("java.util.concurrent.locks.ReadWriteLock", "writeLock"));
shouldNotBeIgnored.add(new QMethod("java.util.concurrent.locks.Condition", "await"));
shouldNotBeIgnored.add(new QMethod("java.util.concurrent.CountDownLatch", "await"));
shouldNotBeIgnored.add(new QMethod("java.util.concurrent.locks.Condition", "awaitUntil"));
shouldNotBeIgnored.add(new QMethod("java.util.concurrent.locks.Condition", "awaitNanos"));
shouldNotBeIgnored.add(new QMethod("java.util.concurrent.Semaphore", "tryAcquire"));
shouldNotBeIgnored.add(new QMethod("java.util.concurrent.locks.Lock", "tryLock"));
shouldNotBeIgnored.add(new QMethod("java.util.concurrent.locks.Lock", "newCondition"));
shouldNotBeIgnored.add(new QMethod("java.util.concurrent.locks.Lock", "tryLock"));
shouldNotBeIgnored.add(new QMethod("java.util.concurrent.BlockingQueue", "offer"));
shouldNotBeIgnored.add(new QMethod("java.util.concurrent.ConcurrentLinkedQueue", "offer"));
shouldNotBeIgnored.add(new QMethod("java.util.concurrent.DelayQueue", "offer"));
shouldNotBeIgnored.add(new QMethod("java.util.concurrent.LinkedBlockingQueue", "offer"));
shouldNotBeIgnored.add(new QMethod("java.util.LinkedList", "offer"));
shouldNotBeIgnored.add(new QMethod("java.util.Queue", "offer"));
shouldNotBeIgnored.add(new QMethod("java.util.concurrent.ArrayBlockingQueue", "offer"));
shouldNotBeIgnored.add(new QMethod("java.util.concurrent.SynchronousQueue", "offer"));
shouldNotBeIgnored.add(new QMethod("java.util.PriorityQueue", "offer"));
shouldNotBeIgnored.add(new QMethod("java.util.concurrent.PriorityBlockingQueue", "offer"));
shouldNotBeIgnored.add(new QMethod("java.util.concurrent.BlockingQueue", "poll"));
shouldNotBeIgnored.add(new QMethod("java.util.Queue", "poll"));
shouldNotBeIgnored.add(new QMethod("java.lang.String", "getBytes"));
shouldNotBeIgnored.add(new QMethod("java.lang.String", "charAt"));
shouldNotBeIgnored.add(new QMethod("java.lang.String", "toString"));
shouldNotBeIgnored.add(new QMethod("java.lang.String", "length"));
shouldNotBeIgnored.add(new QMethod("java.lang.String", "matches"));
shouldNotBeIgnored.add(new QMethod("java.lang.String", "intern"));
shouldNotBeIgnored.add(new QMethod("java.lang.String", "<init>"));
shouldNotBeIgnored.add(new QMethod("java.math.BigDecimal", "inflate"));
shouldNotBeIgnored.add(new QMethod("java.math.BigDecimal", "precision"));
shouldNotBeIgnored.add(new QMethod("java.math.BigDecimal", "toBigIntegerExact"));
shouldNotBeIgnored.add(new QMethod("java.math.BigDecimal", "longValueExact"));
shouldNotBeIgnored.add(new QMethod("java.math.BigDecimal", "intValueExact"));
shouldNotBeIgnored.add(new QMethod("java.math.BigDecimal", "shortValueExact"));
shouldNotBeIgnored.add(new QMethod("java.math.BigDecimal", "byteValueExact"));
shouldNotBeIgnored.add(new QMethod("java.math.BigDecimal", "<init>"));
shouldNotBeIgnored.add(new QMethod("java.math.BigDecimal", "intValue"));
shouldNotBeIgnored.add(new QMethod("java.math.BigDecimal", "stripZerosToMatchScale"));
shouldNotBeIgnored.add(new QMethod("java.math.BigInteger", "addOne"));
shouldNotBeIgnored.add(new QMethod("java.math.BigInteger", "subN"));
shouldNotBeIgnored.add(new QMethod("java.math.BigInteger", "<init>"));
shouldNotBeIgnored.add(new QMethod("java.net.InetAddress", "getByName"));
shouldNotBeIgnored.add(new QMethod("java.net.InetAddress", "getAllByName"));
shouldNotBeIgnored.add(new QMethod("java.lang.ProcessBuilder", "redirectErrorStream"));
shouldNotBeIgnored.add(new QMethod("java.sql.Statement", "executeQuery"));
shouldNotBeIgnored.add(new QMethod("java.sql.PreparedStatement", "executeQuery"));
shouldNotBeIgnored.add(new QMethod("java.util.concurrent.LinkedBlockingQueue", "offer"));
shouldNotBeIgnored.add(new QMethod("java.util.Queue", "offer"));
shouldNotBeIgnored.add(new QMethod("java.util.concurrent.ArrayBlockingQueue", "offer"));
shouldNotBeIgnored.add(new QMethod("java.util.concurrent.SynchronousQueue", "offer"));
shouldNotBeIgnored.add(new QMethod("java.util.concurrent.ExecutorService", "submit"));
shouldNotBeIgnored.add(new QMethod("java.io.File", "createNewFile"));
shouldNotBeIgnored.add(new QMethod("java.io.File", "delete"));
shouldNotBeIgnored.add(new QMethod("java.io.File", "mkdir"));
shouldNotBeIgnored.add(new QMethod("java.io.File", "mkdirs"));
shouldNotBeIgnored.add(new QMethod("java.io.File", "renameTo"));
shouldNotBeIgnored.add(new QMethod("java.io.File", "setLastModified"));
shouldNotBeIgnored.add(new QMethod("java.io.File", "setReadOnly"));
shouldNotBeIgnored.add(new QMethod("java.io.File", "setWritable"));
}
private enum TriStatus {
UNRESOLVED, TRUE, FALSE
}
private enum QuickFixType {
STORE_TO_NEW_LOCAL(descriptionForNewLocal), STORE_TO_SELF(descriptionForStoreToSelf),
WRAP_WITH_IF(descriptionForWrapIf), WRAP_WITH_NEGATED_IF(descriptionForNegatedWrapIf);
private String description;
QuickFixType(String d) {
description = d;
}
public String getDescription() {
return description;
}
}
private QuickFixType quickFixType;
private ImportRewrite typeSource;
@Override
public void setOptions(Map<String, String> options) {
quickFixType = QuickFixType.valueOf(options.get("resolutionType"));
}
@Override
protected boolean resolveBindings() {
return true;
}
@Override
protected ASTVisitor getApplicabilityVisitor() {
return new ReturnValueResolutionVisitor();
}
@Override
protected ASTVisitor getCustomLabelVisitor() {
return new ReturnValueResolutionVisitor();
}
@Override
@SuppressFBWarnings(value = "BAS_BLOATED_ASSIGNMENT_SCOPE",
justification = "the call to getLabel() is needed to fill out information for custom descriptions")
public String getDescription() {
if (description == null) {
String label = getLabel(); // force traversing, which fills in description
if (description == null) {
return label; // something funky is happening, description shouldn't be null
// We'll be safe and return label (which is not null)
}
}
return description;
}
@Override
protected void repairBug(ASTRewrite rewrite, CompilationUnit workingUnit, BugInstance bug) throws BugResolutionException {
ASTNode node = getASTNode(workingUnit, bug.getPrimarySourceLineAnnotation());
this.typeSource = ImportRewrite.create(workingUnit, true); // these imports won't get added automatically
ReturnValueResolutionVisitor rvrFinder = new ReturnValueResolutionVisitor();
node.accept(rvrFinder);
Statement fixedStatement = makeFixedStatement(rewrite, rvrFinder);
if (fixedStatement != null && rvrFinder.badMethodInvocation != null) {
// we have to call getParent() to get the statement.
// If we simply replace the MethodInvocation with the ifStatement (or whatever),
// we get an extra semicolon on the end.
rewrite.replace(rvrFinder.badMethodInvocation.getParent(), fixedStatement, null);
}
// this is the easiest way to make the new imports (from the type conversion)
// actually be added
addImports(rewrite, workingUnit, typeSource.getAddedImports());
}
private Statement makeFixedStatement(ASTRewrite rewrite, ReturnValueResolutionVisitor rvrFinder) {
switch (quickFixType) {
case STORE_TO_NEW_LOCAL:
return makeVariableDeclarationFragment(rewrite, rvrFinder);
case STORE_TO_SELF:
return makeSelfAssignment(rewrite, rvrFinder);
case WRAP_WITH_IF:
return makeIfStatement(rewrite, rvrFinder, false);
case WRAP_WITH_NEGATED_IF:
return makeIfStatement(rewrite, rvrFinder, true);
default:
System.err.println("Couldn't make a fixed statement? " + rvrFinder.badMethodInvocation);
return null;
}
}
private Statement makeSelfAssignment(ASTRewrite rewrite, ReturnValueResolutionVisitor rvrFinder) {
AST rootNode = rewrite.getAST();
Assignment newAssignment = rootNode.newAssignment();
Expression leftExpression = rvrFinder.badMethodInvocation.getExpression();
while (leftExpression instanceof MethodInvocation) {
leftExpression = ((MethodInvocation) leftExpression).getExpression();
}
newAssignment.setLeftHandSide((Expression) rewrite.createCopyTarget(leftExpression));
newAssignment.setRightHandSide((Expression) rewrite.createCopyTarget(
rvrFinder.badMethodInvocation));
return rootNode.newExpressionStatement(newAssignment);
}
private Statement makeVariableDeclarationFragment(ASTRewrite rewrite, ReturnValueResolutionVisitor rvrFinder) {
AST rootNode = rewrite.getAST();
VariableDeclarationFragment fragment = rootNode.newVariableDeclarationFragment();
fragment.setInitializer((Expression) rewrite.createMoveTarget(rvrFinder.badMethodInvocation));
fragment.setName(rootNode.newSimpleName("local"));
VariableDeclarationStatement retVal = rootNode.newVariableDeclarationStatement(fragment);
Type type = getTypeFromTypeBinding(rvrFinder.badMethodInvocation.resolveTypeBinding(), rootNode);
retVal.setType(type);
return retVal;
}
/*
* A "proper" way to get the type from a type binding. It allows the import to be added
* if it doesn't exist. The return value can be used to write new nodes.
*/
private Type getTypeFromTypeBinding(ITypeBinding typeBinding, AST rootNode) {
return typeSource.addImport(typeBinding, rootNode);
}
@SuppressWarnings("unchecked")
private Statement makeIfStatement(ASTRewrite rewrite, ReturnValueResolutionVisitor rvrFinder, boolean b) {
AST rootNode = rewrite.getAST();
IfStatement ifStatement = rootNode.newIfStatement();
Expression expression = makeIfExpression(rewrite, rvrFinder, b);
ifStatement.setExpression(expression);
// the block surrounds the inner statement with {}
Block thenBlock = rootNode.newBlock();
Statement thenStatement = makeExceptionalStatement(rootNode);
thenBlock.statements().add(thenStatement);
ifStatement.setThenStatement(thenBlock);
return ifStatement;
}
private Expression makeIfExpression(ASTRewrite rewrite, ReturnValueResolutionVisitor rvrFinder, boolean isNegated) {
if (isNegated)
{
AST rootNode = rewrite.getAST();
PrefixExpression negation = rootNode.newPrefixExpression();
negation.setOperator(PrefixExpression.Operator.NOT);
negation.setOperand((Expression) rewrite.createMoveTarget(rvrFinder.badMethodInvocation));
return negation;
}
return (Expression) rewrite.createMoveTarget(rvrFinder.badMethodInvocation);
}
@SuppressWarnings("unchecked")
private Statement makeExceptionalStatement(AST rootNode) {
// makes a statement `System.out.println("Exceptional return value");`
QualifiedName sysout = rootNode.newQualifiedName(rootNode.newSimpleName("System"), rootNode.newSimpleName("out"));
StringLiteral literal = rootNode.newStringLiteral();
literal.setLiteralValue("Exceptional return value");
MethodInvocation expression = rootNode.newMethodInvocation();
expression.setExpression(sysout);
expression.setName(rootNode.newSimpleName("println"));
expression.arguments().add(literal);
return rootNode.newExpressionStatement(expression);
}
private class ReturnValueResolutionVisitor extends ASTVisitor implements ApplicabilityVisitor, CustomLabelVisitor {
private TriStatus returnsSelf = TriStatus.UNRESOLVED;
private String returnTypeOfMethod;
private MethodInvocation badMethodInvocation;
@Override
public boolean visit(MethodInvocation node) {
if (badMethodInvocation != null) {
return false; // only need to go one layer deep. By definition,
// if the return value is ignored, it's not nested in anything
}
QMethod qMethod = QMethod.make(node);
String returnType = node.resolveTypeBinding().getQualifiedName();
// check for the special cases in shouldNotBeIgnored
if (shouldNotBeIgnored.contains(qMethod) && !"void".equals(returnType)) {
badMethodInvocation = node;
this.returnTypeOfMethod = returnType;
// look at the returned value and see if it equals the same type
// as what the method is invoked on.
if (qMethod.qualifiedTypeString.equals(returnTypeOfMethod)) {
returnsSelf = TriStatus.TRUE;
} else {
returnsSelf = TriStatus.FALSE;
}
}
// check for any immutableType methods that return something of the same type
if (immutableTypes.contains(returnType) && qMethod.qualifiedTypeString.equals(returnType)) {
returnsSelf = TriStatus.TRUE;
badMethodInvocation = node;
this.returnTypeOfMethod = returnType;
}
// only need to go one layer deep. By definition,
// if the return value is ignored, it's not nested in anything
return false;
}
@Override
public boolean isApplicable() {
// This answers the question did we find something to replace?
switch (quickFixType) {
case STORE_TO_NEW_LOCAL:
return badMethodInvocation != null;
case STORE_TO_SELF:
return returnsSelf == TriStatus.TRUE;
case WRAP_WITH_IF:
case WRAP_WITH_NEGATED_IF:
return "boolean".equals(returnTypeOfMethod);
default:
return false;
}
}
@Override
public String getLabelReplacement() {
// The if fixes have both a custom label and a custom description.
// Everything else has a nice static label.
switch (quickFixType) {
case STORE_TO_NEW_LOCAL:
case STORE_TO_SELF:
default:
description = quickFixType.getDescription();
return "";
case WRAP_WITH_IF:
case WRAP_WITH_NEGATED_IF:
String methodSourceCode = badMethodInvocation != null ? badMethodInvocation.toString() : "[method call]";
description = quickFixType.getDescription().replace("YYY", methodSourceCode);
// it's okay to invoke toString() here because it's user facing, not actually being turned into code
return methodSourceCode;
}
}
}
}