/* * Copyright 2013 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.refaster; import static java.util.logging.Level.FINE; import com.google.common.base.Optional; import com.google.common.collect.ImmutableClassToInstanceMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Ordering; import com.google.errorprone.fixes.Fix; import com.google.errorprone.fixes.SuggestedFix; import com.google.errorprone.refaster.PlaceholderMethod.PlaceholderExpressionKey; import com.google.errorprone.refaster.UTypeVar.TypeWithExpression; import com.google.errorprone.refaster.annotation.NoAutoboxing; import com.sun.source.tree.Tree.Kind; import com.sun.tools.javac.code.Kinds.KindSelector; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Symbol.MethodSymbol; import com.sun.tools.javac.code.Symtab; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.Type.ForAll; import com.sun.tools.javac.code.Type.MethodType; import com.sun.tools.javac.code.Types; import com.sun.tools.javac.comp.Attr; import com.sun.tools.javac.comp.AttrContext; import com.sun.tools.javac.comp.Enter; import com.sun.tools.javac.comp.Env; import com.sun.tools.javac.comp.Resolve; import com.sun.tools.javac.tree.EndPosTable; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.tree.JCTree.JCAnnotation; import com.sun.tools.javac.tree.JCTree.JCCatch; import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; import com.sun.tools.javac.tree.JCTree.JCExpression; import com.sun.tools.javac.tree.JCTree.JCExpressionStatement; import com.sun.tools.javac.tree.JCTree.JCLiteral; import com.sun.tools.javac.tree.JCTree.JCMethodInvocation; import com.sun.tools.javac.tree.JCTree.JCTry; import com.sun.tools.javac.tree.Pretty; import com.sun.tools.javac.tree.TreeMaker; import com.sun.tools.javac.util.Context; import com.sun.tools.javac.util.JCDiagnostic; import com.sun.tools.javac.util.List; import com.sun.tools.javac.util.ListBuffer; import com.sun.tools.javac.util.Log; import com.sun.tools.javac.util.Position; import com.sun.tools.javac.util.Warner; import java.io.IOException; import java.io.Serializable; import java.io.Writer; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.logging.Logger; import javax.annotation.Nullable; /** * Abstract superclass for templates that can be used to search and replace in a Java syntax tree. * * @author lowasser@google.com (Louis Wasserman) * @param <M> Type of a match for this template. */ public abstract class Template<M extends TemplateMatch> implements Serializable { private static final Logger logger = Logger.getLogger(Template.class.toString()); public static final boolean AUTOBOXING_DEFAULT = true; public abstract ImmutableClassToInstanceMap<Annotation> annotations(); public abstract ImmutableList<UTypeVar> templateTypeVariables(); public abstract ImmutableMap<String, UType> expressionArgumentTypes(); public abstract Iterable<M> match(JCTree tree, Context context); public abstract Fix replace(M match); Iterable<UTypeVar> typeVariables(Context context) { ImmutableList<UTypeVar> ruleTypeVars = context.get(RefasterRule.RULE_TYPE_VARS); return Iterables.concat( (ruleTypeVars == null) ? ImmutableList.<UTypeVar>of() : ruleTypeVars, templateTypeVariables()); } boolean autoboxing() { return !annotations().containsKey(NoAutoboxing.class); } /** * Returns a list of the expected types to be matched. This consists of the argument types from * the @BeforeTemplate method, concatenated with the return types of expression placeholders, * sorted by the name of the placeholder method. * * @throws CouldNotResolveImportException if a referenced type could not be resolved */ protected List<Type> expectedTypes(Inliner inliner) throws CouldNotResolveImportException { ArrayList<Type> result = new ArrayList<>(); ImmutableList<UType> types = expressionArgumentTypes().values().asList(); ImmutableList<String> argNames = expressionArgumentTypes().keySet().asList(); for (int i = 0; i < argNames.size(); i++) { String argName = argNames.get(i); Optional<JCExpression> singleBinding = inliner.getOptionalBinding(new UFreeIdent.Key(argName)); if (!singleBinding.isPresent()) { Optional<java.util.List<JCExpression>> exprs = inliner.getOptionalBinding(new URepeated.Key(argName)); if (!exprs.isPresent() || exprs.get().isEmpty()) { // It is a repeated template variable and matches no expressions. continue; } } result.add(types.get(i).inline(inliner)); } for (PlaceholderExpressionKey key : Ordering.natural().immutableSortedCopy( Iterables.filter(inliner.bindings.keySet(), PlaceholderExpressionKey.class))) { result.add(key.method.returnType().inline(inliner)); } return List.from(result); } /** * Returns a list of the actual types to be matched. This consists of the types of the * expressions bound to the @BeforeTemplate method parameters, concatenated with the types * of the expressions bound to expression placeholders, sorted by the name of the placeholder * method. */ protected List<Type> actualTypes(Inliner inliner) { ArrayList<Type> result = new ArrayList<>(); ImmutableList<String> argNames = expressionArgumentTypes().keySet().asList(); for (int i = 0; i < expressionArgumentTypes().size(); i++) { String argName = argNames.get(i); Optional<JCExpression> singleBinding = inliner.getOptionalBinding(new UFreeIdent.Key(argName)); if (singleBinding.isPresent()) { result.add(singleBinding.get().type); } else { Optional<java.util.List<JCExpression>> exprs = inliner.getOptionalBinding(new URepeated.Key(argName)); if (exprs.isPresent() && !exprs.get().isEmpty()) { Type[] exprTys = new Type[exprs.get().size()]; for (int j = 0; j < exprs.get().size(); j++) { exprTys[j] = exprs.get().get(j).type; } // Get the least upper bound of the types of all expressions that the argument matches. // In the special case where exprs is empty, returns the "bottom" type, which is a // subtype of everything. result.add(inliner.types().lub(List.from(exprTys))); } } } for (PlaceholderExpressionKey key : Ordering.natural().immutableSortedCopy( Iterables.filter(inliner.bindings.keySet(), PlaceholderExpressionKey.class))) { result.add(inliner.getBinding(key).type); } return List.from(result); } @Nullable protected Optional<Unifier> typecheck(Unifier unifier, Inliner inliner, Warner warner, List<Type> expectedTypes, List<Type> actualTypes) { try { ImmutableList<UTypeVar> freeTypeVars = freeTypeVars(unifier); infer(warner, inliner, inliner.<Type>inlineList(freeTypeVars), expectedTypes, inliner.symtab().voidType, actualTypes); for (UTypeVar var : freeTypeVars) { Type instantiationForVar = infer(warner, inliner, inliner.<Type>inlineList(freeTypeVars), expectedTypes, var.inline(inliner), actualTypes); unifier.putBinding(var.key(), TypeWithExpression.create(instantiationForVar.getReturnType())); } if (!checkBounds(unifier, inliner, warner)) { return Optional.absent(); } return Optional.of(unifier); } catch (CouldNotResolveImportException e) { logger.log(FINE, "Failure to resolve an import", e); return Optional.absent(); } catch (InferException e) { logger.log(FINE, "No valid instantiation found: " + e.getMessage()); return Optional.absent(); } } private boolean checkBounds(Unifier unifier, Inliner inliner, Warner warner) throws CouldNotResolveImportException { Types types = unifier.types(); ListBuffer<Type> varsBuffer = new ListBuffer<>(); ListBuffer<Type> bindingsBuffer = new ListBuffer<>(); for (UTypeVar typeVar : typeVariables(unifier.getContext())) { varsBuffer.add(inliner.inlineAsVar(typeVar)); bindingsBuffer.add(unifier.getBinding(typeVar.key()).type()); } List<Type> vars = varsBuffer.toList(); List<Type> bindings = bindingsBuffer.toList(); for (UTypeVar typeVar : typeVariables(unifier.getContext())) { List<Type> bounds = types.getBounds(inliner.inlineAsVar(typeVar)); bounds = types.subst(bounds, vars, bindings); if (!types.isSubtypeUnchecked(unifier.getBinding(typeVar.key()).type(), bounds, warner)) { logger.log(FINE, String.format("%s is not a subtype of %s", inliner.getBinding(typeVar.key()), bounds)); return false; } } return true; } protected static Pretty pretty(Context context, final Writer writer) { final JCCompilationUnit unit = context.get(JCCompilationUnit.class); try { final String unitContents = unit.getSourceFile().getCharContent(false).toString(); return new Pretty(writer, true) { { // Work-around for b/22196513 width = 0; } @Override public void visitAnnotation(JCAnnotation anno) { if (anno.getArguments().isEmpty()) { try { print("@"); printExpr(anno.annotationType); } catch (IOException e) { // the supertype swallows exceptions too throw new RuntimeException(e); } } else { super.visitAnnotation(anno); } } @Override public void printExpr(JCTree tree, int prec) throws IOException { EndPosTable endPositions = unit.endPositions; /* * Modifiers, and specifically flags like final, appear to just need weird special * handling. * * Note: we can't use {@code TreeInfo.getEndPos()} or {@code JCTree.getEndPosition()} * here, because they will return the end position of an enclosing AST node for trees * whose real end positions aren't stored. */ int endPos = endPositions.getEndPos(tree); boolean hasRealEndPosition = endPos != Position.NOPOS; if (tree.getKind() != Kind.MODIFIERS && hasRealEndPosition) { writer.append(unitContents.substring(tree.getStartPosition(), endPos)); } else { super.printExpr(tree, prec); } } @Override public void visitApply(JCMethodInvocation tree) { JCExpression select = tree.getMethodSelect(); if (select != null && select.toString().equals("Refaster.emitCommentBefore")) { String commentLiteral = (String) ((JCLiteral) tree.getArguments().get(0)).getValue(); JCExpression expr = tree.getArguments().get(1); try { print("/* " + commentLiteral + " */ "); } catch (IOException e) { throw new RuntimeException(e); } expr.accept(this); } else { super.visitApply(tree); } } @Override public void printStat(JCTree tree) throws IOException { if (tree instanceof JCExpressionStatement && ((JCExpressionStatement) tree).getExpression() instanceof JCMethodInvocation) { JCMethodInvocation invocation = (JCMethodInvocation) ((JCExpressionStatement) tree).getExpression(); JCExpression select = invocation.getMethodSelect(); if (select != null && select.toString().equals("Refaster.emitComment")) { String commentLiteral = (String) ((JCLiteral) invocation.getArguments().get(0)).getValue(); print("// " + commentLiteral); return; } } super.printStat(tree); } @Override public void visitTry(JCTry tree) { if (tree.getResources().isEmpty()) { super.visitTry(tree); return; } try { print("try ("); boolean first = true; for (JCTree resource : tree.getResources()) { if (!first) { print(";"); println(); } printExpr(resource); first = false; } print(")"); printStat(tree.body); for (JCCatch catchStmt : tree.getCatches()) { printStat(catchStmt); } if (tree.getFinallyBlock() != null) { print(" finally "); printStat(tree.getFinallyBlock()); } } catch (IOException e) { throw new RuntimeException(e); } } }; } catch (IOException e) { throw new RuntimeException(e); } } private static class InferException extends Exception { final Collection<JCDiagnostic> diagnostics; public InferException(Collection<JCDiagnostic> diagnostics) { this.diagnostics = diagnostics; } @Override public String getMessage() { return "Inference failed with the following error(s): " + diagnostics.toString(); } } /** * Returns the inferred method type of the template based on the given actual argument types. * * @throws InferException if no instances of the specified type variables would allow the * {@code actualArgTypes} to match the {@code expectedArgTypes} */ private Type infer(Warner warner, Inliner inliner, List<Type> freeTypeVariables, List<Type> expectedArgTypes, Type returnType, List<Type> actualArgTypes) throws InferException { Symtab symtab = inliner.symtab(); Type methodType = new MethodType(expectedArgTypes, returnType, List.<Type>nil(), symtab.methodClass); if (!freeTypeVariables.isEmpty()) { methodType = new ForAll(freeTypeVariables, methodType); } Enter enter = inliner.enter(); MethodSymbol methodSymbol = new MethodSymbol(0, inliner.asName("__m__"), methodType, symtab.unknownSymbol); Type site = symtab.methodClass.type; Env<AttrContext> env = enter.getTopLevelEnv( TreeMaker.instance(inliner.getContext()).TopLevel(List.<JCTree>nil())); // Set up the resolution phase: try { Field field = AttrContext.class.getDeclaredField("pendingResolutionPhase"); field.setAccessible(true); field.set(env.info, newMethodResolutionPhase(autoboxing())); } catch (ReflectiveOperationException e) { throw new LinkageError(e.getMessage(), e); } Object resultInfo; try { Class<?> resultInfoClass = Class.forName("com.sun.tools.javac.comp.Attr$ResultInfo"); Constructor<?> resultInfoCtor = resultInfoClass.getDeclaredConstructor(Attr.class, KindSelector.class, Type.class); resultInfoCtor.setAccessible(true); resultInfo = resultInfoCtor.newInstance( Attr.instance(inliner.getContext()), KindSelector.PCK, Type.noType); } catch (ReflectiveOperationException e) { throw new LinkageError(e.getMessage(), e); } // Type inference sometimes produces diagnostics, so we need to catch them to avoid interfering // with the enclosing compilation. Log.DeferredDiagnosticHandler handler = new Log.DeferredDiagnosticHandler(Log.instance(inliner.getContext())); try { MethodType result = callCheckMethod(warner, inliner, resultInfo, actualArgTypes, methodSymbol, site, env); if (!handler.getDiagnostics().isEmpty()) { throw new InferException(handler.getDiagnostics()); } return result; } finally { Log.instance(inliner.getContext()).popDiagnosticHandler(handler); } } /** * Reflectively instantiate the package-private {@code MethodResolutionPhase} enum. */ private static Object newMethodResolutionPhase(boolean autoboxing) { for (Class<?> c : Resolve.class.getDeclaredClasses()) { if (!c.getName().equals("com.sun.tools.javac.comp.Resolve$MethodResolutionPhase")) { continue; } for (Object e : c.getEnumConstants()) { if (e.toString().equals(autoboxing ? "BOX" : "BASIC")) { return e; } } } return null; } /** * Reflectively invoke Resolve.checkMethod(), which despite being package-private is apparently * the only useful entry-point into javac8's type inference implementation. */ private MethodType callCheckMethod(Warner warner, Inliner inliner, Object resultInfo, List<Type> actualArgTypes, MethodSymbol methodSymbol, Type site, Env<AttrContext> env) throws InferException { try { Method checkMethod; checkMethod = Resolve.class.getDeclaredMethod( "checkMethod", Env.class, Type.class, Symbol.class, Class.forName("com.sun.tools.javac.comp.Attr$ResultInfo"), // ResultInfo is package-private List.class, List.class, Warner.class); checkMethod.setAccessible(true); return (MethodType) checkMethod.invoke(Resolve.instance(inliner.getContext()), env, site, methodSymbol, resultInfo, actualArgTypes, /*freeTypeVariables=*/List.<Type>nil(), warner); } catch (InvocationTargetException e) { if (e.getCause() instanceof Resolve.InapplicableMethodException) { throw new InferException(ImmutableList.of( ((Resolve.InapplicableMethodException) e.getTargetException()).getDiagnostic())); } throw new LinkageError(e.getMessage(), e.getCause()); } catch (ReflectiveOperationException e) { throw new LinkageError(e.getMessage(), e); } } /** * Returns a list of the elements of {@code typeVariables} that are <em>not</em> bound in the * specified {@link Unifier}. */ private ImmutableList<UTypeVar> freeTypeVars(Unifier unifier) { ImmutableList.Builder<UTypeVar> builder = ImmutableList.builder(); for (UTypeVar var : typeVariables(unifier.getContext())) { if (unifier.getBinding(var.key()) == null) { builder.add(var); } } return builder.build(); } protected static Fix addImports(Inliner inliner, SuggestedFix.Builder fix) { for (String importToAdd : inliner.getImportsToAdd()) { fix.addImport(importToAdd); } for (String staticImportToAdd : inliner.getStaticImportsToAdd()) { fix.addStaticImport(staticImportToAdd); } return fix.build(); } }