com.google.errorprone.fixes.SuggestedFixes.java Source code

Java tutorial

Introduction

Here is the source code for com.google.errorprone.fixes.SuggestedFixes.java

Source

/*
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.errorprone.fixes;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Iterables.getLast;
import static com.google.errorprone.util.ASTHelpers.getAnnotation;
import static com.google.errorprone.util.ASTHelpers.getAnnotationWithSimpleName;
import static com.google.errorprone.util.ASTHelpers.getModifiers;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
import static com.sun.source.tree.Tree.Kind.ASSIGNMENT;
import static com.sun.source.tree.Tree.Kind.NEW_ARRAY;
import static com.sun.source.tree.Tree.Kind.PARENTHESIZED;
import static com.sun.tools.javac.code.TypeTag.CLASS;

import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Predicates;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.errorprone.VisitorState;
import com.google.errorprone.fixes.SuggestedFix.Builder;
import com.google.errorprone.util.ErrorProneToken;
import com.sun.source.doctree.DocTree;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ModifiersTree;
import com.sun.source.tree.NewArrayTree;
import com.sun.source.tree.ParenthesizedTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.DocTreePath;
import com.sun.source.util.TreePath;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.parser.Tokens;
import com.sun.tools.javac.tree.DCTree;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.tree.TreeScanner;
import com.sun.tools.javac.util.Position;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.StreamSupport;
import javax.annotation.Nullable;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.SimpleTypeVisitor8;

/** Factories for constructing {@link Fix}es. */
public class SuggestedFixes {

    /** Parse a modifier token into a {@link Modifier}. */
    @Nullable
    private static Modifier getTokModifierKind(ErrorProneToken tok) {
        switch (tok.kind()) {
        case PUBLIC:
            return Modifier.PUBLIC;
        case PROTECTED:
            return Modifier.PROTECTED;
        case PRIVATE:
            return Modifier.PRIVATE;
        case ABSTRACT:
            return Modifier.ABSTRACT;
        case STATIC:
            return Modifier.STATIC;
        case FINAL:
            return Modifier.FINAL;
        case TRANSIENT:
            return Modifier.TRANSIENT;
        case VOLATILE:
            return Modifier.VOLATILE;
        case SYNCHRONIZED:
            return Modifier.SYNCHRONIZED;
        case NATIVE:
            return Modifier.NATIVE;
        case STRICTFP:
            return Modifier.STRICTFP;
        case DEFAULT:
            return Modifier.DEFAULT;
        default:
            return null;
        }
    }

    /** Add modifiers to the given class, method, or field declaration. */
    @Nullable
    public static SuggestedFix addModifiers(Tree tree, VisitorState state, Modifier... modifiers) {
        ModifiersTree originalModifiers = getModifiers(tree);
        if (originalModifiers == null) {
            return null;
        }
        Set<Modifier> toAdd = Sets.difference(new TreeSet<>(Arrays.asList(modifiers)),
                originalModifiers.getFlags());
        if (originalModifiers.getFlags().isEmpty()) {
            int pos = state.getEndPosition(originalModifiers) != Position.NOPOS
                    ? state.getEndPosition(originalModifiers) + 1
                    : ((JCTree) tree).getStartPosition();
            int base = ((JCTree) tree).getStartPosition();
            java.util.Optional<Integer> insert = state.getTokensForNode(tree).stream()
                    .map(token -> token.pos() + base).filter(thisPos -> thisPos >= pos).findFirst();
            int insertPos = insert.orElse(pos); // shouldn't ever be able to get to the else
            return SuggestedFix.replace(insertPos, insertPos, Joiner.on(' ').join(toAdd) + " ");
        }
        // a map from modifiers to modifier position (or -1 if the modifier is being added)
        // modifiers are sorted in Google Java Style order
        Map<Modifier, Integer> modifierPositions = new TreeMap<>();
        for (Modifier mod : toAdd) {
            modifierPositions.put(mod, -1);
        }
        List<ErrorProneToken> tokens = state.getTokensForNode(originalModifiers);
        int base = ((JCTree) originalModifiers).getStartPosition();
        for (ErrorProneToken tok : tokens) {
            Modifier mod = getTokModifierKind(tok);
            if (mod != null) {
                modifierPositions.put(mod, base + tok.pos());
            }
        }
        SuggestedFix.Builder fix = SuggestedFix.builder();
        // walk the map of all modifiers, and accumulate a list of new modifiers to insert
        // beside an existing modifier
        List<Modifier> modifiersToWrite = new ArrayList<>();
        for (Modifier mod : modifierPositions.keySet()) {
            int p = modifierPositions.get(mod);
            if (p == -1) {
                modifiersToWrite.add(mod);
            } else if (!modifiersToWrite.isEmpty()) {
                fix.replace(p, p, Joiner.on(' ').join(modifiersToWrite) + " ");
                modifiersToWrite.clear();
            }
        }
        if (!modifiersToWrite.isEmpty()) {
            fix.postfixWith(originalModifiers, " " + Joiner.on(' ').join(modifiersToWrite));
        }
        return fix.build();
    }

    /** Remove modifiers from the given class, method, or field declaration. */
    @Nullable
    public static Fix removeModifiers(Tree tree, VisitorState state, Modifier... modifiers) {
        Set<Modifier> toRemove = ImmutableSet.copyOf(modifiers);
        ModifiersTree originalModifiers = getModifiers(tree);
        if (originalModifiers == null) {
            return null;
        }
        SuggestedFix.Builder fix = SuggestedFix.builder();
        List<ErrorProneToken> tokens = state.getTokensForNode(originalModifiers);
        int basePos = ((JCTree) originalModifiers).getStartPosition();
        boolean empty = true;
        for (ErrorProneToken tok : tokens) {
            Modifier mod = getTokModifierKind(tok);
            if (toRemove.contains(mod)) {
                empty = false;
                fix.replace(basePos + tok.pos(), basePos + tok.endPos() + 1, "");
                break;
            }
        }
        if (empty) {
            return null;
        }
        return fix.build();
    }

    /** Returns a human-friendly name of the given type for use in fixes. */
    public static String qualifyType(VisitorState state, SuggestedFix.Builder fix, TypeMirror type) {
        TreeMaker make = TreeMaker.instance(state.context)
                .forToplevel((JCTree.JCCompilationUnit) state.getPath().getCompilationUnit());
        return qualifyType(make, fix, type);
    }

    /** Returns a human-friendly name of the given {@link Symbol.TypeSymbol} for use in fixes. */
    public static String qualifyType(VisitorState state, SuggestedFix.Builder fix, Symbol.TypeSymbol sym) {
        if (sym.getKind() == ElementKind.TYPE_PARAMETER) {
            return sym.getSimpleName().toString();
        }
        TreeMaker make = TreeMaker.instance(state.context)
                .forToplevel((JCTree.JCCompilationUnit) state.getPath().getCompilationUnit());
        return qualifyType(make, fix, sym);
    }

    /**
     * Returns a human-friendly name of the given {@link Symbol.TypeSymbol} for use in fixes.
     *
     * <ul>
     * <li>If the type is already imported, its simple name is used.
     * <li>If an enclosing type is imported, that enclosing type is used as a qualified.
     * <li>Otherwise the outermost enclosing type is imported and used as a qualifier.
     * </ul>
     */
    public static String qualifyType(TreeMaker make, SuggestedFix.Builder fix, Symbol.TypeSymbol sym) {
        // let javac figure out whether the type is already imported
        JCTree.JCExpression qual = make.QualIdent(sym);
        if (!selectsPackage(qual)) {
            return qual.toString();
        }
        Deque<String> names = new ArrayDeque<>();
        Symbol curr = sym;
        while (true) {
            names.addFirst(curr.getSimpleName().toString());
            if (curr.owner == null || curr.owner.getKind() == ElementKind.PACKAGE) {
                break;
            }
            curr = curr.owner;
        }
        fix.addImport(curr.toString());
        return Joiner.on('.').join(names);
    }

    public static String qualifyType(final TreeMaker make, SuggestedFix.Builder fix, TypeMirror type) {
        return type.accept(new SimpleTypeVisitor8<String, SuggestedFix.Builder>() {
            @Override
            protected String defaultAction(TypeMirror e, Builder builder) {
                return e.toString();
            }

            @Override
            public String visitArray(ArrayType t, Builder builder) {
                return t.getComponentType().accept(this, builder) + "[]";
            }

            @Override
            public String visitDeclared(DeclaredType t, Builder builder) {
                String baseType = qualifyType(make, builder, ((Type) t).tsym);
                if (t.getTypeArguments().isEmpty()) {
                    return baseType;
                }
                StringBuilder b = new StringBuilder(baseType);
                b.append('<');
                boolean started = false;
                for (TypeMirror arg : t.getTypeArguments()) {
                    if (started) {
                        b.append(',');
                    }
                    b.append(arg.accept(this, builder));
                    started = true;
                }
                b.append('>');
                return b.toString();
            }
        }, fix);
    }

    /** Returns true iff the given expression is qualified by a package. */
    private static boolean selectsPackage(JCTree.JCExpression qual) {
        JCTree.JCExpression curr = qual;
        while (true) {
            Symbol sym = getSymbol(curr);
            if (sym != null && sym.getKind() == ElementKind.PACKAGE) {
                return true;
            }
            if (!(curr instanceof JCTree.JCFieldAccess)) {
                break;
            }
            curr = ((JCTree.JCFieldAccess) curr).getExpression();
        }
        return false;
    }

    /** Replaces the leaf doctree in the given path with {@code replacement}. */
    public static void replaceDocTree(SuggestedFix.Builder fix, DocTreePath docPath, String replacement) {
        DocTree leaf = docPath.getLeaf();
        checkArgument(leaf instanceof DCTree.DCEndPosTree, "no end position information for %s", leaf.getKind());
        DCTree.DCEndPosTree<?> node = (DCTree.DCEndPosTree<?>) leaf;
        DCTree.DCDocComment comment = (DCTree.DCDocComment) docPath.getDocComment();
        fix.replace((int) node.getSourcePosition(comment), node.getEndPos(comment), replacement);
    }

    /**
     * Fully qualifies a javadoc reference, e.g. for replacing {@code {@link List}} with
     * {@code {@link java.util.List}}
     *
     * @param fix the fix builder to add to
     * @param docPath the path to a {@link DCTree.DCReference} element
     */
    public static void qualifyDocReference(SuggestedFix.Builder fix, DocTreePath docPath, VisitorState state) {

        DocTree leaf = docPath.getLeaf();
        checkArgument(leaf.getKind() == DocTree.Kind.REFERENCE, "expected a path to a reference, got %s instead",
                leaf.getKind());
        DCTree.DCReference reference = (DCTree.DCReference) leaf;

        Symbol sym = (Symbol) JavacTrees.instance(state.context).getElement(docPath);
        if (sym == null) {
            return;
        }
        String refString = reference.toString();
        String qualifiedName;
        int idx = refString.indexOf('#');
        if (idx >= 0) {
            qualifiedName = sym.owner.getQualifiedName() + refString.substring(idx, refString.length());
        } else {
            qualifiedName = sym.getQualifiedName().toString();
        }

        replaceDocTree(fix, docPath, qualifiedName);
    }

    /**
     * Returns a {@link Fix} that adds members defined by {@code firstMember} (and optionally {@code
     * otherMembers}) to the end of the class referenced by {@code classTree}.  This method should
     * only be called once per {@link ClassTree} as the suggestions will otherwise collide.
     */
    public static Fix addMembers(ClassTree classTree, VisitorState state, String firstMember,
            String... otherMembers) {
        checkNotNull(classTree);
        int classEndPosition = state.getEndPosition(classTree);
        StringBuilder stringBuilder = new StringBuilder();
        for (String memberSnippet : Lists.asList(firstMember, otherMembers)) {
            stringBuilder.append("\n\n").append(memberSnippet);
        }
        stringBuilder.append('\n');

        return SuggestedFix.replace(classEndPosition - 1, classEndPosition - 1, stringBuilder.toString());
    }

    /**
     * Renames the given {@link VariableTree} and its usages in the current compilation unit to {@code
     * replacement}.
     */
    public static Fix renameVariable(VariableTree tree, final String replacement, VisitorState state) {
        String name = tree.getName().toString();
        int typeLength = state.getSourceForNode(tree.getType()).length();
        int pos = ((JCTree) tree).getStartPosition() + state.getSourceForNode(tree).indexOf(name, typeLength);
        final SuggestedFix.Builder fix = SuggestedFix.builder().replace(pos, pos + name.length(), replacement);
        final Symbol.VarSymbol sym = getSymbol(tree);
        ((JCTree) state.getPath().getCompilationUnit()).accept(new TreeScanner() {
            @Override
            public void visitIdent(JCTree.JCIdent tree) {
                if (sym.equals(getSymbol(tree))) {
                    fix.replace(tree, replacement);
                }
            }
        });
        return fix.build();
    }

    /** Deletes the given exceptions from a method's throws clause. */
    public static Fix deleteExceptions(MethodTree tree, final VisitorState state, List<ExpressionTree> toDelete) {
        List<? extends ExpressionTree> trees = tree.getThrows();
        if (toDelete.size() == trees.size()) {
            return SuggestedFix.replace(getThrowsPosition(tree, state), state.getEndPosition(getLast(trees)), "");
        }
        String replacement = FluentIterable.from(tree.getThrows()).filter(Predicates.not(Predicates.in(toDelete)))
                .transform(new Function<ExpressionTree, String>() {
                    @Override
                    @Nullable
                    public String apply(ExpressionTree input) {
                        return state.getSourceForNode(input);
                    }
                }).join(Joiner.on(", "));
        return SuggestedFix.replace(((JCTree) tree.getThrows().get(0)).getStartPosition(),
                state.getEndPosition(getLast(tree.getThrows())), replacement);
    }

    private static int getThrowsPosition(MethodTree tree, VisitorState state) {
        for (ErrorProneToken token : state.getTokensForNode(tree)) {
            if (token.kind() == Tokens.TokenKind.THROWS) {
                return ((JCTree) tree).getStartPosition() + token.pos();
            }
        }
        throw new AssertionError();
    }

    /**
     * Returns a fix that adds a {@code @SuppressWarnings(warningToSuppress)} to the closest
     * suppressible element to the node pointed at by {@code state.getPath()}.
     *
     * <p>If the closest suppressible element already has a @SuppressWarning annotation,
     * warningToSuppress will be added to the value in {@code @SuppressWarnings} instead.
     *
     * <p>In the event that a suppressible element couldn't be found (e.g.: the state is pointing at a
     * CompilationUnit, or some other internal inconsistency has occurred), or the enclosing
     * suppressible element already has a {@code @SuppressWarnings} annotation with {@code
     * warningToSuppress}, this method will return null.
     */
    @Nullable
    public static Fix addSuppressWarnings(VisitorState state, String warningToSuppress) {
        Builder fixBuilder = SuggestedFix.builder();
        addSuppressWarnings(fixBuilder, state, warningToSuppress);
        return fixBuilder.isEmpty() ? null : fixBuilder.build();
    }

    /**
     * Modifies {@code fixBuilder} to either create a new {@code @SuppressWarnings} element on the
     * closest suppressible node, or add {@code warningToSuppress} to that node if there's already a
     * {@code SuppressWarnings} annotation there.
     *
     * @see #addSuppressWarnings(VisitorState, String)
     */
    public static void addSuppressWarnings(Builder fixBuilder, VisitorState state, String warningToSuppress) {
        // Find the nearest tree to add @SuppressWarnings to.
        Tree suppressibleNode = suppressibleNode(state.getPath());
        if (suppressibleNode == null) {
            return;
        }

        SuppressWarnings existingAnnotation = getAnnotation(suppressibleNode, SuppressWarnings.class);
        // If we have an existing @SuppressWarnings on the element, extend its value
        if (existingAnnotation != null) {
            // Add warning to the existing annotation
            String[] values = existingAnnotation.value();
            if (Arrays.asList(values).contains(warningToSuppress)) {
                // The nearest suppress warnings already contains this thing, so we can't add another thing
                return;
            }
            AnnotationTree suppressAnnotationTree = getAnnotationWithSimpleName(
                    findAnnotationsTree(suppressibleNode), SuppressWarnings.class.getSimpleName());
            if (suppressAnnotationTree == null) {
                // This is weird, bail out
                return;
            }

            fixBuilder.merge(addValuesToAnnotationArgument(suppressAnnotationTree, "value",
                    ImmutableList.of(state.getTreeMaker().Literal(CLASS, warningToSuppress).toString()), state));
        } else {
            // Otherwise, add a suppress annotation to the element
            fixBuilder.prefixWith(suppressibleNode, "@SuppressWarnings(\"" + warningToSuppress + "\")\n");
        }
    }

    private static List<? extends AnnotationTree> findAnnotationsTree(Tree tree) {
        ModifiersTree maybeModifiers = getModifiers(tree);
        return maybeModifiers == null ? ImmutableList.of() : maybeModifiers.getAnnotations();
    }

    @Nullable
    private static Tree suppressibleNode(TreePath path) {
        return StreamSupport.stream(path.spliterator(), false).filter(
                tree -> tree instanceof MethodTree || tree instanceof ClassTree || tree instanceof VariableTree)
                .findFirst().orElse(null);
    }

    /**
     * Returns a fix that appends {@code newValues} to the {@code parameterName} argument for {@code
     * annotation}, regardless of whether there is already an argument.
     *
     * <p>N.B.: {@code newValues} are source-code strings, not string literal values.
     */
    public static Builder addValuesToAnnotationArgument(AnnotationTree annotation, String parameterName,
            Collection<String> newValues, VisitorState state) {
        if (annotation.getArguments().isEmpty()) {
            String parameterPrefix = parameterName.equals("value") ? "" : (parameterName + " = ");
            return SuggestedFix.builder().replace(annotation, annotation.toString().replaceFirst("\\(\\)",
                    "(" + parameterPrefix + newArgument(newValues) + ")"));
        }
        Optional<ExpressionTree> maybeExistingArgument = findArgument(annotation, parameterName);
        if (!maybeExistingArgument.isPresent()) {
            return SuggestedFix.builder().prefixWith(annotation.getArguments().get(0),
                    parameterName + " = " + newArgument(newValues) + ", ");
        }

        ExpressionTree existingArgument = maybeExistingArgument.get();
        if (!existingArgument.getKind().equals(NEW_ARRAY)) {
            return SuggestedFix.builder().replace(existingArgument,
                    newArgument(state.getSourceForNode(existingArgument), newValues));
        }

        NewArrayTree newArray = (NewArrayTree) existingArgument;
        if (newArray.getInitializers().isEmpty()) {
            return SuggestedFix.builder().replace(newArray, newArgument(newValues));
        } else {
            return SuggestedFix.builder().postfixWith(getLast(newArray.getInitializers()),
                    ", " + Joiner.on(", ").join(newValues));
        }
    }

    private static String newArgument(String existingParameters, Collection<String> initializers) {
        return newArgument(
                new ImmutableList.Builder<String>().add(existingParameters).addAll(initializers).build());
    }

    private static String newArgument(Collection<String> initializers) {
        StringBuilder expression = new StringBuilder();
        if (initializers.size() > 1) {
            expression.append('{');
        }
        Joiner.on(", ").appendTo(expression, initializers);
        if (initializers.size() > 1) {
            expression.append('}');
        }
        return expression.toString();
    }

    private static Optional<ExpressionTree> findArgument(AnnotationTree annotation, String parameter) {
        for (ExpressionTree argument : annotation.getArguments()) {
            if (argument.getKind().equals(ASSIGNMENT)) {
                AssignmentTree assignment = (AssignmentTree) argument;
                if (assignment.getVariable().toString().equals(parameter)) {
                    ExpressionTree expression = assignment.getExpression();
                    while (expression.getKind().equals(PARENTHESIZED)) {
                        expression = ((ParenthesizedTree) expression).getExpression();
                    }
                    return Optional.of(expression);
                }
            }
        }
        return Optional.absent();
    }
}