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.Nullable; 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.Assignment; import org.eclipse.jdt.core.dom.BodyDeclaration; 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.ExpressionStatement; import org.eclipse.jdt.core.dom.FieldAccess; import org.eclipse.jdt.core.dom.FieldDeclaration; import org.eclipse.jdt.core.dom.IBinding; import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.IVariableBinding; import org.eclipse.jdt.core.dom.MethodDeclaration; import org.eclipse.jdt.core.dom.MethodInvocation; import org.eclipse.jdt.core.dom.QualifiedName; import org.eclipse.jdt.core.dom.SimpleName; import org.eclipse.jdt.core.dom.Statement; import org.eclipse.jdt.core.dom.Type; import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.jdt.core.dom.TypeLiteral; import org.eclipse.jdt.core.dom.VariableDeclarationFragment; import org.eclipse.jdt.core.dom.VariableDeclarationStatement; import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; import util.TraversalUtil; public class UseEnumCollectionsResolution extends BugResolution { @Override protected boolean resolveBindings() { return true; } @Override protected ASTVisitor getCustomLabelVisitor() { return new EnumCollectionsVisitor(); } @Override protected void repairBug(ASTRewrite rewrite, CompilationUnit workingUnit, BugInstance bug) throws BugResolutionException { ASTNode node = getASTNode(workingUnit, bug.getPrimarySourceLineAnnotation()); EnumCollectionsVisitor visitor = new EnumCollectionsVisitor(); node.accept(visitor); Expression newEnumConstructor = makeEnumConstructor(visitor, rewrite); rewrite.replace(visitor.badConstructorUsage, newEnumConstructor, null); if (visitor.isMap) { addImports(rewrite, workingUnit, "java.util.EnumMap"); } else { addImports(rewrite, workingUnit, "java.util.EnumSet"); } } private Expression makeEnumConstructor(EnumCollectionsVisitor visitor, ASTRewrite rewrite) { AST ast = rewrite.getAST(); TypeLiteral enumDotClass = ast.newTypeLiteral(); // this is the EnumHere.class enumDotClass.setType(ast.newSimpleType(ast.newName(visitor.enumNameToUse))); if (visitor.isMap) { return makeEnumMap(enumDotClass, ast); } return makeEnumSet(enumDotClass, ast); } @SuppressWarnings("unchecked") private Expression makeEnumSet(TypeLiteral enumType, AST ast) { MethodInvocation newEnumSet = ast.newMethodInvocation(); newEnumSet.setExpression(ast.newSimpleName("EnumSet")); newEnumSet.setName(ast.newSimpleName("noneOf")); newEnumSet.arguments().add(enumType); return newEnumSet; } @SuppressWarnings("unchecked") private Expression makeEnumMap(TypeLiteral enumType, AST ast) { ClassInstanceCreation newEnumMap = ast.newClassInstanceCreation(); Type enumMap = ast.newSimpleType(ast.newName("EnumMap")); // makes the <> braces by default newEnumMap.setType(ast.newParameterizedType(enumMap)); newEnumMap.arguments().add(enumType); return newEnumMap; } private static class EnumCollectionsVisitor extends ASTVisitor implements CustomLabelVisitor { public String enumNameToUse; public ClassInstanceCreation badConstructorUsage; private SimpleName badCollectionName; public boolean isMap; @Override public boolean visit(MethodInvocation node) { if (badConstructorUsage != null) { return false; } Expression methodReciever = node.getExpression(); if (isEnumBasedMap(methodReciever) || isEnumBasedSet(methodReciever)) { try { badCollectionName = findNameOfCollection(methodReciever); isMap = isEnumBasedMap(methodReciever); enumNameToUse = getEnumFromBindingOrNestedBinding(node.getExpression().resolveTypeBinding()); badConstructorUsage = findInitializerOfCollection(badCollectionName, node); } catch (EnumParsingException e) { // reset, parsing out the enum usages went poorly badCollectionName = null; enumNameToUse = null; badConstructorUsage = null; } } return true; } private ClassInstanceCreation findInitializerOfCollection(SimpleName collectionName, MethodInvocation startOfSearch) throws EnumParsingException { IBinding collectionBinding = collectionName.resolveBinding(); // if collectionName does not refer to a variable, we won't be able to find // where it is being initialized if (collectionBinding instanceof IVariableBinding) { if (((IVariableBinding) collectionBinding).isField()) { return findFieldInitialization(collectionName, TraversalUtil.findClosestAncestor(startOfSearch, TypeDeclaration.class)); } return findMethodInitialization(collectionName, TraversalUtil.findClosestAncestor(startOfSearch, MethodDeclaration.class), false); } throw new EnumParsingException(); } @SuppressWarnings("unchecked") private ClassInstanceCreation findMethodInitialization(SimpleName collectionName, MethodDeclaration methodDeclaration, boolean isField) throws EnumParsingException { // Look in this method for an initialization of the variable collectionName // This handles initialization of a field (i.e. something w/o a declaration) // and a local variable (i.e. something with a declaration) List<Statement> statements = methodDeclaration.getBody().statements(); for (Statement statement : statements) { if (!isField && statement instanceof VariableDeclarationStatement) { return findClassInstanceCreationInDeclaration(collectionName, (VariableDeclarationStatement) statement); } else if (isField && statement instanceof ExpressionStatement) { return findClassInstanceCreationInAssignment(collectionName, (ExpressionStatement) statement); } } throw new EnumParsingException(); } private ClassInstanceCreation findClassInstanceCreationInAssignment(SimpleName collectionName, ExpressionStatement statement) throws EnumParsingException { // this ExpressionStatement is expected to be a wrapped assignment Expression expression = statement.getExpression(); if (expression instanceof Assignment) { Assignment assignment = (Assignment) expression; if (areNamesEqual(collectionName, findNameOfCollection(assignment.getLeftHandSide()))) { if (assignment.getRightHandSide() instanceof ClassInstanceCreation) { return (ClassInstanceCreation) assignment.getRightHandSide(); } } } throw new EnumParsingException(); } @SuppressWarnings("unchecked") private ClassInstanceCreation findClassInstanceCreationInDeclaration(SimpleName collectionName, VariableDeclarationStatement statement) throws EnumParsingException { List<VariableDeclarationFragment> fragments = statement.fragments(); for (VariableDeclarationFragment fragment : fragments) { if (areNamesEqual(collectionName, fragment.getName())) { Expression initializer = fragment.getInitializer(); if (initializer instanceof ClassInstanceCreation) { return (ClassInstanceCreation) initializer; } else { throw new EnumParsingException(); } } } throw new EnumParsingException(); } private static boolean areNamesEqual(SimpleName firstName, SimpleName secondName) { // can't do collectionName.equals(fragment.getName()) because the SimpleName // equals method is just a pointer equality, so we have to compare the identifiers return firstName.getIdentifier().equals(secondName.getIdentifier()); } @SuppressWarnings("unchecked") private ClassInstanceCreation findFieldInitialization(SimpleName collectionName, TypeDeclaration typeDeclaration) throws EnumParsingException { // this will look through all the fields for the declaration. If it wasn't // initialized at declaration, we'll try looking in the default constructor. for (FieldDeclaration field : typeDeclaration.getFields()) { List<VariableDeclarationFragment> fragments = field.fragments(); for (VariableDeclarationFragment fragment : fragments) { if (areNamesEqual(collectionName, fragment.getName())) { Expression initializer = fragment.getInitializer(); // if there are cases other than "is ClassInstanceCreation" // or "null", I can't think of them. if (initializer instanceof ClassInstanceCreation) { return (ClassInstanceCreation) initializer; } else { return lookInDefaultConstructor(collectionName, typeDeclaration); } } } } throw new EnumParsingException(); } @SuppressWarnings("unchecked") private ClassInstanceCreation lookInDefaultConstructor(SimpleName collectionName, TypeDeclaration typeDeclaration) throws EnumParsingException { // Search all bodyDeclarations for a methodDeclaration with no arguments // i.e. default constructor List<BodyDeclaration> bodyDeclarations = typeDeclaration.bodyDeclarations(); for (BodyDeclaration declaration : bodyDeclarations) { if (declaration instanceof MethodDeclaration) { MethodDeclaration methodDeclaration = ((MethodDeclaration) declaration); if (methodDeclaration.isConstructor() && methodDeclaration.parameters().isEmpty()) { return findMethodInitialization(collectionName, methodDeclaration, true); } } } throw new EnumParsingException(); } // Looks through either this binding or any nested arguments and returns the first enum found, if any // Does not throw an exception, but rather returns null. This is to facilitate checking multiple bindings. @Nullable private String getEnumFromBindingOrNestedBinding(ITypeBinding binding) { if (binding.isEnum()) { return binding.getName(); } ITypeBinding[] argumentBindings = binding.getTypeArguments(); if (argumentBindings == null) { return null; } for (int i = 0; i < argumentBindings.length; i++) { String name = getEnumFromBindingOrNestedBinding(argumentBindings[i]); if (name != null) { return name; } } return null; } private boolean isEnumBasedSet(Expression expression) { // A bit hackish, but this looks at the name of the type and if there is also an enum in one of the TypeParameters ITypeBinding binding = expression.resolveTypeBinding(); return binding.getQualifiedName().matches("java\\.util.*Set.*") && null != getEnumFromBindingOrNestedBinding(binding); } private boolean isEnumBasedMap(Expression expression) { // A bit hackish, but this looks at the name of the type and if there is also an enum in one of the TypeParameters ITypeBinding binding = expression.resolveTypeBinding(); return binding.getQualifiedName().matches("java\\.util.*Map.*") && null != getEnumFromBindingOrNestedBinding(binding); } private SimpleName findNameOfCollection(Expression methodReciever) throws EnumParsingException { if (methodReciever instanceof SimpleName) { return (SimpleName) methodReciever; } if (methodReciever instanceof QualifiedName) { return ((QualifiedName) methodReciever).getName(); } if (methodReciever instanceof FieldAccess) { return ((FieldAccess) methodReciever).getName(); } throw new EnumParsingException(); } @Override public String getLabelReplacement() { return badCollectionName.getIdentifier() + " to be an " + (isMap ? "EnumMap" : "EnumSet"); } } private static class EnumParsingException extends Exception { private static final long serialVersionUID = -5995607690601671285L; } }