/*
* 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.errorprone.fixes.Fix;
import com.google.errorprone.fixes.SuggestedFix;
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.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Symtab;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Type.MethodType;
import com.sun.tools.javac.code.Types;
import com.sun.tools.javac.comp.Enter;
import com.sun.tools.javac.comp.Infer.InferenceException;
import com.sun.tools.javac.comp.Infer.NoInstanceException;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCAnnotation;
import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.tree.Pretty;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.ListBuffer;
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.util.Map;
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> typeVariables();
public abstract ImmutableMap<String, UType> expressionArgumentTypes();
public abstract Iterable<M> match(JCTree tree, Context context);
public abstract Fix replace(M match);
boolean autoboxing() {
return !annotations().containsKey(NoAutoboxing.class);
}
/**
* Returns a list of the expected types of the expression arguments, in order.
* (This is equivalent to the list of argument types of the @BeforeTemplate method.)
*
* @throws CouldNotResolveImportException if a referenced type could not be resolved
*/
protected List<Type> expectedTypes(Inliner inliner) throws CouldNotResolveImportException {
Type[] result = new Type[expressionArgumentTypes().size()];
ImmutableList<UType> types = expressionArgumentTypes().values().asList();
ImmutableList<String> argNames = expressionArgumentTypes().keySet().asList();
for (int i = 0; i < result.length; 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[i] = types.get(i).inline(inliner);
}
return List.from(result);
}
/**
* Returns a list of the actual types of the expression arguments, in order. (This expects the
* expression arguments to have already been captured, and for the {@link Inliner} to contain
* those bindings.)
*/
protected List<Type> actualTypes(Inliner inliner) {
Type[] result = new Type[expressionArgumentTypes().size()];
ImmutableList<String> argNames = expressionArgumentTypes().keySet().asList();
for (int i = 0; i < result.length; i++) {
String argName = argNames.get(i);
Optional<JCExpression> singleBinding =
inliner.getOptionalBinding(new UFreeIdent.Key(argName));
if (singleBinding.isPresent()) {
result[i] = singleBinding.get().type;
} else {
Optional<java.util.List<JCExpression>> exprs =
inliner.getOptionalBinding(new URepeated.Key(argName));
if (exprs.isPresent() && !exprs.get().isEmpty()) {
// The argument matches 1 or more expressions.
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.
result[i] = inliner.types().lub(List.from(exprTys));
}
}
}
return List.from(result);
}
@Nullable
protected 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, UTypeVar>inlineList(freeTypeVars),
expectedTypes,
inliner.symtab().voidType,
actualTypes);
for (UTypeVar var : freeTypeVars) {
Type instantiationForVar = infer(warner,
inliner,
inliner.<Type, UTypeVar>inlineList(freeTypeVars),
expectedTypes,
var.inline(inliner),
actualTypes);
unifier.putBinding(var.key(),
TypeWithExpression.create(instantiationForVar.getReturnType()));
}
if (!checkBounds(unifier, inliner, warner)) {
return null;
}
return unifier;
} catch (CouldNotResolveImportException e) {
logger.log(FINE, "Failure to resolve an import", e);
return null;
} catch (InferenceException e) {
logger.log(FINE, "No valid instantiation found: " + e.getDiagnostic());
return null;
}
}
private boolean checkBounds(Unifier unifier, Inliner inliner, Warner warner)
throws CouldNotResolveImportException {
Types types = unifier.types();
ListBuffer<Type> varsBuffer = ListBuffer.lb();
ListBuffer<Type> bindingsBuffer = ListBuffer.lb();
for (UTypeVar typeVar : typeVariables()) {
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()) {
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) {
@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 {
Map<JCTree, Integer> endPositions = unit.endPositions;
/*
* Modifiers, and specifically flags like final, appear to just need weird special
* handling.
*/
if (tree.getKind() != Kind.MODIFIERS && endPositions.containsKey(tree)) {
writer.append(unitContents.substring(
tree.getStartPosition(), tree.getEndPosition(endPositions)));
} else {
super.printExpr(tree, prec);
}
}
};
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Returns the inferred method type of the template based on the given actual argument types.
*
* @throws NoInstanceException 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 NoInstanceException {
Symtab symtab = inliner.symtab();
MethodType methodType =
new MethodType(expectedArgTypes, returnType, List.<Type>nil(), symtab.methodClass);
Enter enter = inliner.enter();
MethodSymbol methodSymbol =
new MethodSymbol(0, inliner.asName("__m__"), methodType, symtab.unknownSymbol);
return inliner.infer().instantiateMethod(enter.getEnv(methodType.tsym),
freeTypeVariables,
methodType,
methodSymbol,
actualArgTypes,
autoboxing(),
false,
warner);
}
/**
* 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()) {
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();
}
}