Java tutorial
/* * 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(); } }