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.List; import javax.annotation.CheckForNull; 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.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.Block; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.EnhancedForStatement; import org.eclipse.jdt.core.dom.Expression; import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.MethodInvocation; import org.eclipse.jdt.core.dom.ParameterizedType; import org.eclipse.jdt.core.dom.SimpleName; import org.eclipse.jdt.core.dom.SimpleType; import org.eclipse.jdt.core.dom.SingleVariableDeclaration; import org.eclipse.jdt.core.dom.Statement; 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.ImportUtil; import util.TraversalUtil; public class EntrySetResolution extends BugResolution { private ImportRewrite typeSource; private ASTRewrite rewrite; private AST ast; private Type keyType; private Type valueType; private SimpleName entryName; private EntrySetResolutionVisitor descriptionVisitor = new EntrySetResolutionVisitor(); private SimpleName newValueVariableName; @Override protected boolean resolveBindings() { return true; } @Override protected ASTVisitor getCustomLabelVisitor() { return descriptionVisitor; } @Override public String getDescription() { if (descriptionVisitor != null && descriptionVisitor.ancestorForLoop != null) { // calls to toString() are okay here because this isn't going to be used as // actual code SingleVariableDeclaration key = descriptionVisitor.ancestorForLoop.getParameter(); String keyType = key.getType().toString(); String keyVar = key.getName().toString(); String valueType = descriptionVisitor.valueTypeName; String valueVar = "tempVar"; if (descriptionVisitor.badMapGetVariableFragment != null) valueVar = descriptionVisitor.badMapGetVariableFragment.getName().toString(); String mapName = ((MethodInvocation) descriptionVisitor.ancestorForLoop.getExpression()).getExpression().toString(); return String.format("for(Map.Entry<%s,%s> entry : %s.entrySet()) {<br/>" + "%s %s = entry.getKey();<br/>" + "%s %s = entry.getValue();<br/>" + "...<br/>" + "}", keyType, valueType, mapName, keyType, keyVar, valueType, valueVar ); } return super.getDescription(); } private Type getTypeFromTypeBinding(ITypeBinding typeBinding, AST ast) { return typeSource.addImport(typeBinding, ast); } @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 this.rewrite = rewrite; this.ast = rewrite.getAST(); EntrySetResolutionVisitor visitor = new EntrySetResolutionVisitor(); node.accept(visitor); EnhancedForStatement replacement = makeReplacementForLoop(visitor); rewrite.replace(visitor.ancestorForLoop, replacement, null); addImports(rewrite, workingUnit, ImportUtil.filterOutJavaLangImports(typeSource.getAddedImports())); addImports(rewrite, workingUnit, "java.util.Map"); // shouldn't be necessary. Allows us to use Map.entry } @SuppressWarnings("unchecked") private EnhancedForStatement makeReplacementForLoop(EntrySetResolutionVisitor visitor) { // this would be map.keySet(). // We need this to get the type of map and get the variable name of map MethodInvocation oldLoopExpression = (MethodInvocation) visitor.ancestorForLoop.getExpression(); // for(Parameter : Expression) EnhancedForStatement replacement = ast.newEnhancedForStatement(); replacement.setParameter(makeEntrySetParameter(oldLoopExpression)); replacement.setExpression(makeCallToEntrySet(oldLoopExpression)); List<Statement> replacementBlockStatements = ((Block) replacement.getBody()).statements(); // create new statement to replace the key object (e.g. the String s that used to be in the for each) replacementBlockStatements.add(makeNewKeyStatement(visitor)); // replace the call to map.get() with a call to entry.getValue() replacementBlockStatements.add(makeNewValueStatement(visitor)); // transfer the rest of the statements in the old block copyRestOfBlock(replacementBlockStatements, visitor); return replacement; } @SuppressWarnings("unchecked") private void copyRestOfBlock(List<Statement> replacementBlockStatements, EntrySetResolutionVisitor visitor) { List<Statement> oldBlockStatements = ((Block) visitor.ancestorForLoop.getBody()).statements(); for (Statement statement : oldBlockStatements) { if (statement.equals(visitor.badMapGetStatement)) { if (visitor.badMapGetMethodInvocation == null) { // this was the old variable statement and we can continue; // just ignore it because we have already fixed it } // else, fix the anonymous get usage (replace it with the name) replacementBlockStatements.add((Statement) rewrite.createMoveTarget(statement)); rewrite.replace(visitor.badMapGetMethodInvocation, newValueVariableName, null); continue; } replacementBlockStatements.add((Statement) rewrite.createMoveTarget(statement)); } } private VariableDeclarationStatement makeNewVariableStatement(SimpleName varName, String initMethodName, Type varType) { VariableDeclarationFragment keyFragment = ast.newVariableDeclarationFragment(); keyFragment.setName(copy(varName)); MethodInvocation entrySetKey = ast.newMethodInvocation(); entrySetKey.setExpression(copy(this.entryName)); entrySetKey.setName(ast.newSimpleName(initMethodName)); keyFragment.setInitializer(entrySetKey); VariableDeclarationStatement newKeyStatement = ast.newVariableDeclarationStatement(keyFragment); newKeyStatement.setType(copy(varType)); return newKeyStatement; } private VariableDeclarationStatement makeNewKeyStatement(EntrySetResolutionVisitor visitor) { return makeNewVariableStatement(visitor.ancestorForLoop.getParameter().getName(), "getKey", keyType); } private VariableDeclarationStatement makeNewValueStatement(EntrySetResolutionVisitor visitor) { SimpleName nameOfNewVar; if (visitor.badMapGetVariableFragment != null) { nameOfNewVar = visitor.badMapGetVariableFragment.getName(); } else if (valueType instanceof SimpleType) { StringBuilder tempName = new StringBuilder(((SimpleName) ((SimpleType) valueType).getName()).getIdentifier()); tempName.setCharAt(0, Character.toLowerCase(tempName.charAt(0))); nameOfNewVar = ast.newSimpleName(tempName.toString()); } else { nameOfNewVar = ast.newSimpleName("mapValue"); } newValueVariableName = nameOfNewVar; return makeNewVariableStatement(nameOfNewVar, "getValue", valueType); } private SingleVariableDeclaration makeEntrySetParameter(MethodInvocation oldLoopExpression) { // this is the type of map, e.g. Map<String, Integer> ParameterizedType oldParamType = (ParameterizedType) getTypeFromTypeBinding(oldLoopExpression.getExpression() .resolveTypeBinding(), ast); // give it a base type of Map.Entry, then transfer the params ParameterizedType newParamType = ast.newParameterizedType(ast.newSimpleType(ast.newName("Map.Entry"))); transferTypeArguments(oldParamType, newParamType); SingleVariableDeclaration loopParameter = ast.newSingleVariableDeclaration(); loopParameter.setType(newParamType); this.entryName = ast.newSimpleName("entry"); loopParameter.setName(entryName); return loopParameter; } private MethodInvocation makeCallToEntrySet(MethodInvocation expressionToCopyVariableFrom) { MethodInvocation initialization = ast.newMethodInvocation(); // Expression.Name() We want to copy the expression and make a new name initialization.setExpression((Expression) rewrite.createCopyTarget(expressionToCopyVariableFrom.getExpression())); initialization.setName(ast.newSimpleName("entrySet")); return initialization; } @SuppressWarnings("unchecked") private void transferTypeArguments(ParameterizedType existingType, ParameterizedType newType) { List<Type> oldTypeArgs = existingType.typeArguments(); int i = 0; while (!oldTypeArgs.isEmpty()) { // This is the only way I could find to copy the Types. rewrite.createCopyTarget didn't help // because the types seemed to be in a limbo between attached and not attached. // If I try to copy w/o deleting them from the original list, some sort of infinite loop happens // on clone Type oldType = oldTypeArgs.get(0); oldType.delete(); if (i == 0) { this.keyType = copy(oldType); } else if (i == 1) { this.valueType = copy(oldType); } // oldType is okay to add now w/o a clone, because it is detached. newType.typeArguments().add(oldType); i++; } } // Convenience method to copy nodes @SuppressWarnings("unchecked") private <T extends ASTNode> T copy(T original) { return (T) ASTNode.copySubtree(ast, original); } private static class EntrySetResolutionVisitor extends ASTVisitor implements CustomLabelVisitor { public String valueTypeName; public EnhancedForStatement ancestorForLoop; public Statement badMapGetStatement; @CheckForNull public VariableDeclarationFragment badMapGetVariableFragment; // this or badMapGetMethodInvocation will be null @CheckForNull public MethodInvocation badMapGetMethodInvocation; // this or badMapGetVariableFragment will be null @Override public boolean visit(MethodInvocation node) { if (ancestorForLoop != null) { return false; } if (!"get".equals(node.getName().getIdentifier())) { return true; // there may be a nested method invocation } valueTypeName = node.resolveTypeBinding().getName(); // for description message this.ancestorForLoop = TraversalUtil.findClosestAncestor(node, EnhancedForStatement.class); this.badMapGetStatement = TraversalUtil.findClosestAncestor(node, Statement.class); // if this is null, it was an anonymous use this.badMapGetVariableFragment = TraversalUtil.findClosestAncestor(node, VariableDeclarationFragment.class); if (badMapGetVariableFragment == null) { this.badMapGetMethodInvocation = node; // this is an anonymous usage, and will need to be replaced } return false; } @Override public String getLabelReplacement() { return ""; // we only need this to make the description } } }