/* * Copyright 2011 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 com.google.common.collect.ImmutableList; import com.google.errorprone.VisitorState; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; import com.sun.tools.javac.tree.EndPosTable; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; import com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Set; import javax.annotation.Nullable; import javax.lang.model.element.Modifier; /** * @author alexeagle@google.com (Alex Eagle) */ public class SuggestedFix implements Fix { private final ImmutableList<FixOperation> fixes; private final ImmutableList<String> importsToAdd; private final ImmutableList<String> importsToRemove; private SuggestedFix( List<FixOperation> fixes, List<String> importsToAdd, List<String> importsToRemove) { this.fixes = ImmutableList.copyOf(fixes); this.importsToAdd = ImmutableList.copyOf(importsToAdd); this.importsToRemove = ImmutableList.copyOf(importsToRemove); } @Override public boolean isEmpty() { return fixes.isEmpty() && importsToAdd.isEmpty() && importsToRemove.isEmpty(); } @Override public Collection<String> getImportsToAdd() { return importsToAdd; } @Override public Collection<String> getImportsToRemove() { return importsToRemove; } @Override public String toString(JCCompilationUnit compilationUnit) { StringBuilder result = new StringBuilder("replace "); for (Replacement replacement : getReplacements(compilationUnit.endPositions)) { result .append("position " + replacement.startPosition() + ":" + replacement.endPosition()) .append(" with \"" + replacement.replaceWith() + "\" "); } return result.toString(); } @Override public Set<Replacement> getReplacements(EndPosTable endPositions) { if (endPositions == null) { throw new IllegalArgumentException( "Cannot produce correct replacements without endPositions."); } Replacements replacements = new Replacements(); for (FixOperation fix : fixes) { replacements.add(fix.getReplacement(endPositions)); } return replacements.descending(); } /** {@link Builder#replace(Tree, String)} */ public static SuggestedFix replace(Tree tree, String replaceWith) { return builder().replace(tree, replaceWith).build(); } /** * Replace the characters from startPos, inclusive, until endPos, exclusive, with the given * string. * * @param startPos The position from which to start replacing, inclusive * @param endPos The position at which to end replacing, exclusive * @param replaceWith The string to replace with */ public static SuggestedFix replace(int startPos, int endPos, String replaceWith) { return builder().replace(startPos, endPos, replaceWith).build(); } /** * Replace a tree node with a string, but adjust the start and end positions as well. For example, * if the tree node begins at index 10 and ends at index 30, this call will replace the characters * at index 15 through 25 with "replacement": * * <pre> * {@code fix.replace(node, "replacement", 5, -5)} * </pre> * * @param node The tree node to replace * @param replaceWith The string to replace with * @param startPosAdjustment The adjustment to add to the start position (negative is OK) * @param endPosAdjustment The adjustment to add to the end position (negative is OK) */ public static SuggestedFix replace( Tree node, String replaceWith, int startPosAdjustment, int endPosAdjustment) { return builder().replace(node, replaceWith, startPosAdjustment, endPosAdjustment).build(); } /** {@link Builder#prefixWith(Tree, String)} */ public static SuggestedFix prefixWith(Tree node, String prefix) { return builder().prefixWith(node, prefix).build(); } /** {@link Builder#postfixWith(Tree, String)} */ public static SuggestedFix postfixWith(Tree node, String postfix) { return builder().postfixWith(node, postfix).build(); } /** {@link Builder#delete(Tree)} */ public static SuggestedFix delete(Tree node) { return builder().delete(node).build(); } /** {@link Builder#swap(Tree, Tree)} */ public static SuggestedFix swap(Tree node1, Tree node2) { return builder().swap(node1, node2).build(); } public static Builder builder() { return new Builder(); } /** Builds {@link SuggestedFix}s. */ public static class Builder { private final List<FixOperation> fixes = new ArrayList<>(); private final List<String> importsToAdd = new ArrayList<>(); private final List<String> importsToRemove = new ArrayList<>(); protected Builder() {} public boolean isEmpty() { return fixes.isEmpty() && importsToAdd.isEmpty() && importsToRemove.isEmpty(); } public SuggestedFix build() { return new SuggestedFix(fixes, importsToAdd, importsToRemove); } private Builder with(FixOperation fix) { fixes.add(fix); return this; } public Builder replace(Tree node, String replaceWith) { checkNotSyntheticConstructor(node); return with(new ReplacementFix((DiagnosticPosition) node, replaceWith)); } /** * Replace the characters from startPos, inclusive, until endPos, exclusive, with the * given string. * * @param startPos The position from which to start replacing, inclusive * @param endPos The position at which to end replacing, exclusive * @param replaceWith The string to replace with */ public Builder replace(int startPos, int endPos, String replaceWith) { DiagnosticPosition pos = new IndexedPosition(startPos, endPos); return with(new ReplacementFix(pos, replaceWith)); } /** * Replace a tree node with a string, but adjust the start and end positions as well. * For example, if the tree node begins at index 10 and ends at index 30, this call will * replace the characters at index 15 through 25 with "replacement": * <pre> * {@code fix.replace(node, "replacement", 5, -5)} * </pre> * * @param node The tree node to replace * @param replaceWith The string to replace with * @param startPosAdjustment The adjustment to add to the start position (negative is OK) * @param endPosAdjustment The adjustment to add to the end position (negative is OK) */ public Builder replace( Tree node, String replaceWith, int startPosAdjustment, int endPosAdjustment) { checkNotSyntheticConstructor(node); return with(new ReplacementFix( new AdjustedPosition((JCTree) node, startPosAdjustment, endPosAdjustment), replaceWith)); } public Builder prefixWith(Tree node, String prefix) { checkNotSyntheticConstructor(node); return with(new PrefixInsertion((DiagnosticPosition) node, prefix)); } public Builder postfixWith(Tree node, String postfix) { checkNotSyntheticConstructor(node); return with(new PostfixInsertion((DiagnosticPosition) node, postfix)); } public Builder delete(Tree node) { checkNotSyntheticConstructor(node); return replace(node, ""); } public Builder swap(Tree node1, Tree node2) { checkNotSyntheticConstructor(node1); checkNotSyntheticConstructor(node2); // calling Tree.toString() is kind of cheesy, but we don't currently have a better option // TODO(cushon): consider an approach that doesn't rewrite the original tokens fixes.add(new ReplacementFix((DiagnosticPosition) node1, node2.toString())); fixes.add(new ReplacementFix((DiagnosticPosition) node2, node1.toString())); return this; } /** * Add an import statement as part of this SuggestedFix. * Import string should be of the form "foo.bar.baz". */ public Builder addImport(String importString) { importsToAdd.add("import " + importString); return this; } /** * Add a static import statement as part of this SuggestedFix. * Import string should be of the form "foo.bar.baz". */ public Builder addStaticImport(String importString) { importsToAdd.add("import static " + importString); return this; } /** * Remove an import statement as part of this SuggestedFix. * Import string should be of the form "foo.bar.baz". */ public Builder removeImport(String importString) { importsToRemove.add("import " + importString); return this; } /** * Remove a static import statement as part of this SuggestedFix. * Import string should be of the form "foo.bar.baz". */ public Builder removeStaticImport(String importString) { importsToRemove.add("import static " + importString); return this; } /** * Merges all edits from {@code other} into {@code this}. If {@code other} is null, do nothing. */ public Builder merge(@Nullable Builder other) { if (other == null) { return this; } fixes.addAll(other.fixes); importsToAdd.addAll(other.importsToAdd); importsToRemove.addAll(other.importsToRemove); return this; } /** * Merges all edits from {@code other} into {@code this}. If {@code other} is null, do nothing. */ public Builder merge(@Nullable SuggestedFix other) { if (other == null) { return this; } fixes.addAll(other.fixes); importsToAdd.addAll(other.importsToAdd); importsToRemove.addAll(other.importsToRemove); return this; } /** * Implicit default constructors are one of the few synthetic constructs * added to the AST early enough to be visible from Error Prone, so we * do a sanity-check here to prevent attempts to edit them. */ private static void checkNotSyntheticConstructor(Tree tree) { if (tree instanceof MethodTree && ASTHelpers.isGeneratedConstructor((MethodTree) tree)) { throw new AssertionError("Cannot edit synthetic AST nodes"); } } } /** Models a single fix operation. */ private static interface FixOperation { /** Calculate the replacement operation once end positions are available. */ Replacement getReplacement(EndPosTable endPositions); } /** Inserts new text at a specific insertion point (e.g. prefix or postfix). */ private abstract static class InsertionFix implements FixOperation { protected abstract int getInsertionIndex(EndPosTable endPositions); protected final DiagnosticPosition position; protected final String insertion; protected InsertionFix(DiagnosticPosition position, String insertion) { checkArgument(position.getStartPosition() >= 0, "invalid start position"); this.position = position; this.insertion = insertion; } @Override public Replacement getReplacement(EndPosTable endPositions) { int insertionIndex = getInsertionIndex(endPositions); return Replacement.create(insertionIndex, insertionIndex, insertion); } } private static class PostfixInsertion extends InsertionFix { public PostfixInsertion(DiagnosticPosition tree, String insertion) { super(tree, insertion); } @Override protected int getInsertionIndex(EndPosTable endPositions) { return position.getEndPosition(endPositions); } } private static class PrefixInsertion extends InsertionFix { public PrefixInsertion(DiagnosticPosition tree, String insertion) { super(tree, insertion); } @Override protected int getInsertionIndex(EndPosTable endPositions) { return position.getStartPosition(); } } /** Replaces an entire diagnostic position (from start to end) with the given string. */ private static class ReplacementFix implements FixOperation { private final DiagnosticPosition original; private final String replacement; public ReplacementFix(DiagnosticPosition original, String replacement) { checkArgument(original.getStartPosition() >= 0, "invalid start position"); this.original = original; this.replacement = replacement; } @Override public Replacement getReplacement(EndPosTable endPositions) { return Replacement.create( original.getStartPosition(), original.getEndPosition(endPositions), replacement); } } /** @deprecated prefer {@link SuggestedFixes#addModifiers} */ @Deprecated public static Fix addModifier(Tree tree, Modifier modifier, VisitorState state) { return SuggestedFixes.addModifiers(tree, state, modifier); } /** @deprecated prefer {@link SuggestedFixes#removeModifiers} */ @Deprecated public static Fix removeModifier(Tree tree, Modifier modifier, VisitorState state) { return SuggestedFixes.removeModifiers(tree, state, modifier); } }