package quickfix; 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 edu.umd.cs.findbugs.BugInstance; 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.exception.ASTNodeNotFoundException; import edu.umd.cs.findbugs.plugin.eclipse.quickfix.exception.BugResolutionException; import org.eclipse.core.resources.IMarker; import org.eclipse.jdt.core.JavaModelException; 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.CharacterLiteral; 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.InfixExpression; import org.eclipse.jdt.core.dom.MethodInvocation; import org.eclipse.jdt.core.dom.StringLiteral; import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; import util.TraversalUtil; /** * Resolves things like someString + ":" + someInt -> someString + ':' + someInt * and str.replace("a","b") -> str.replace('a','b') * * It handles the concatenation a bit differently from the other case because * sometimes it's okay to just make single length strings into chars, but * other times it's not. See the unit tests for examples. * * @author KevinLubick * */ public class UseCharacterParameterizedMethodResolution extends BugResolution { private boolean useStringBuilder; @Override protected boolean resolveBindings() { return true; } @Override public void setOptions(Map<String, String> options) { useStringBuilder = Boolean.parseBoolean(options.get("useStringBuilder")); } @Override protected ASTVisitor getApplicabilityVisitor() { return new StringToCharVisitor(); } @Override protected ASTNode getNodeForMarker(IMarker marker) throws JavaModelException, ASTNodeNotFoundException { ASTNode originalNode = super.getNodeForMarker(marker); if (originalNode != null) { return TraversalUtil.backtrackToBlock(originalNode); } throw new ASTNodeNotFoundException(); } @Override protected void repairBug(ASTRewrite rewrite, CompilationUnit workingUnit, BugInstance bug) throws BugResolutionException { ASTNode node = getASTNode(workingUnit, bug.getPrimarySourceLineAnnotation()); node = TraversalUtil.backtrackToBlock(node); StringToCharVisitor visitor = new StringToCharVisitor(); node.accept(visitor); AST ast = rewrite.getAST(); if (useStringBuilder) { MethodInvocation toString = buildChainedToString(rewrite, visitor, ast); rewrite.replace(visitor.infixExpression, toString, null); } else { handleSimpleConcat(rewrite, visitor, ast); } // replaces all non-concatenation StringLiterals for (Map.Entry<StringLiteral, Character> entry : visitor.replacements.entrySet()) { CharacterLiteral charLiteral = ast.newCharacterLiteral(); charLiteral.setCharValue(entry.getValue()); rewrite.replace(entry.getKey(), charLiteral, null); } } @SuppressWarnings("unchecked") private MethodInvocation buildChainedToString(ASTRewrite rewrite, StringToCharVisitor visitor, AST ast) { ClassInstanceCreation newStringBuilder = ast.newClassInstanceCreation(); newStringBuilder.setType(ast.newSimpleType(ast.newName("StringBuilder"))); // doesn't need to be qualified because // it's java.lang Expression lastExpression = newStringBuilder; for (Expression expr : visitor.nodesBeingConcatenated) { MethodInvocation newAppend = ast.newMethodInvocation(); // chains the call newAppend.setExpression(lastExpression); Expression fixedExpr = fixSingleLengthString(expr, ast); if (fixedExpr == expr) { // if it wasn't fixed, we have to make a moveTarget newAppend.arguments().add((Expression) rewrite.createMoveTarget(expr)); } else { // if it was fixed, it doesn't need a move target newAppend.arguments().add(fixedExpr); } newAppend.setName(ast.newSimpleName("append")); lastExpression = newAppend; } MethodInvocation toString = ast.newMethodInvocation(); toString.setExpression(lastExpression); toString.setName(ast.newSimpleName("toString")); return toString; } private void handleSimpleConcat(ASTRewrite rewrite, StringToCharVisitor visitor, AST ast) { for (Expression expr : visitor.nodesBeingConcatenated) { Expression fixedExpr = fixSingleLengthString(expr, ast); if (fixedExpr != expr) { // reference equality is fine here rewrite.replace(expr, fixedExpr, null); } } } // returns either the original expression or a new CharacterLiteral that replaced // a length 1 StringLiteral private static Expression fixSingleLengthString(Expression expr, AST ast) { if (expr instanceof StringLiteral) { String literalValue = ((StringLiteral) expr).getLiteralValue(); if (literalValue.length() == 1) { CharacterLiteral charLiteral = ast.newCharacterLiteral(); charLiteral.setCharValue(literalValue.charAt(0)); return charLiteral; } } return expr; } private class StringToCharVisitor extends ASTVisitor implements ApplicabilityVisitor { private static final String STRING_IDENTIFIER = "java.lang.String"; // this stores string expressions being concatenated (if any) public List<Expression> nodesBeingConcatenated = new ArrayList<>(); public InfixExpression infixExpression; // This maps length 1 strings that are not in infixExpressions // to its appropriate replacement public Map<StringLiteral, Character> replacements = new HashMap<>(); @Override public boolean visit(InfixExpression node) { if (!(node.getOperator() == InfixExpression.Operator.PLUS && STRING_IDENTIFIER.equals(node.resolveTypeBinding() .getQualifiedName()))) { return true; } this.infixExpression = node; nodesBeingConcatenated.add(node.getLeftOperand()); nodesBeingConcatenated.add(node.getRightOperand()); @SuppressWarnings("unchecked") List<Expression> extendedOperations = node.extendedOperands(); for (Expression expression : extendedOperations) { nodesBeingConcatenated.add(expression); } return false; // prevent traversal to any String Literals } @Override // finds all string literals in a method argument or similar public boolean visit(StringLiteral node) { if (!(node.getParent() instanceof MethodInvocation)) { // if a StringLiteral is not in a method invocation, we don't care about it (e.g. ternary return false; } String literalValue = node.getLiteralValue(); if (literalValue.length() == 1) { replacements.put(node, literalValue.charAt(0)); } return false; } @Override public boolean isApplicable() { if (useStringBuilder) { return infixExpression != null; } if (!replacements.isEmpty()) { return true; } if (infixExpression == null) { return false; } if (isNonReplaceableString(infixExpression.getLeftOperand())) { return true; } return isNonReplaceableString(infixExpression.getRightOperand()); } // returns true if this expression is a string expression or // a stringliteral that is not length 1 private boolean isNonReplaceableString(Expression expression) { // TODO worry about String constants? I can't really fix them... if (expression instanceof StringLiteral) { if (((StringLiteral) expression).getLiteralValue().length() > 1) { return true; } } else if (STRING_IDENTIFIER.equals(expression.resolveTypeBinding().getQualifiedName())) { return true; } return false; } } }