/*
* 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 com.google.common.base.Preconditions.checkState;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.SEVERE;
import com.google.auto.value.AutoValue;
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.annotation.AlsoNegation;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCArrayAccess;
import com.sun.tools.javac.tree.JCTree.JCAssign;
import com.sun.tools.javac.tree.JCTree.JCAssignOp;
import com.sun.tools.javac.tree.JCTree.JCBinary;
import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
import com.sun.tools.javac.tree.JCTree.JCConditional;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.tree.JCTree.JCFieldAccess;
import com.sun.tools.javac.tree.JCTree.JCInstanceOf;
import com.sun.tools.javac.tree.JCTree.JCTypeCast;
import com.sun.tools.javac.tree.JCTree.JCUnary;
import com.sun.tools.javac.tree.TreeInfo;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.Warner;
import java.io.IOException;
import java.io.StringWriter;
import java.lang.annotation.Annotation;
import java.util.Map;
import java.util.logging.Logger;
import javax.annotation.Nullable;
/**
* Implementation of a template to match and replace an expression anywhere in an AST.
*
* @author lowasser@google.com (Louis Wasserman)
*/
@AutoValue
public abstract class ExpressionTemplate extends Template<ExpressionTemplateMatch>
implements Unifiable<JCExpression> {
private static final Logger logger = Logger.getLogger(ExpressionTemplate.class.toString());
public static ExpressionTemplate create(
UExpression expression, UType returnType) {
return create(ImmutableMap.<String, UType>of(), expression, returnType);
}
public static ExpressionTemplate create(
Map<String, ? extends UType> expressionArgumentTypes,
UExpression expression, UType returnType) {
return create(
ImmutableClassToInstanceMap.<Annotation>builder().build(),
ImmutableList.<UTypeVar>of(),
expressionArgumentTypes,
expression,
returnType);
}
public static ExpressionTemplate create(
ImmutableClassToInstanceMap<Annotation> annotations,
Iterable<UTypeVar> typeVariables,
Map<String, ? extends UType> expressionArgumentTypes,
UExpression expression, UType returnType) {
return new AutoValue_ExpressionTemplate(
annotations,
ImmutableList.copyOf(typeVariables),
ImmutableMap.copyOf(expressionArgumentTypes),
expression,
returnType);
}
abstract UExpression expression();
abstract UType returnType();
public boolean generateNegation() {
return annotations().containsKey(AlsoNegation.class);
}
public ExpressionTemplate negation() {
checkState(returnType().equals(UPrimitiveType.BOOLEAN),
"Return type must be boolean to generate negation, but was %s", returnType());
return create(annotations(), typeVariables(), expressionArgumentTypes(),
expression().negate(), returnType());
}
/**
* Returns the matches of this template against the specified target AST.
*/
@Override
public Iterable<ExpressionTemplateMatch> match(JCTree target, Context context) {
if (target instanceof JCExpression) {
JCExpression targetExpr = (JCExpression) target;
Unifier unifier = unify(targetExpr, new Unifier(context));
if (unifier != null) {
return ImmutableList.of(new ExpressionTemplateMatch(targetExpr, unifier));
}
}
return ImmutableList.of();
}
@Override
@Nullable
public Unifier unify(JCExpression target, Unifier unifier) {
unifier = expression().unify(target, unifier);
if (unifier != null) {
Inliner inliner = unifier.createInliner();
try {
List<Type> expectedTypes = expectedTypes(inliner);
List<Type> actualTypes = actualTypes(inliner);
/*
* The Java compiler's type inference doesn't directly take into account the expected
* return type, so we test the return type by treating the expected return type as an
* extra method argument, and the actual type of the return expression as its actual
* value.
*/
expectedTypes = expectedTypes.prepend(returnType().inline(inliner));
actualTypes = actualTypes.prepend(target.type);
return typecheck(unifier, inliner, new Warner(target), expectedTypes, actualTypes);
} catch (CouldNotResolveImportException e) {
logger.log(FINE, "Failure to resolve import", e);
}
}
return null;
}
/**
* Generates a {@link SuggestedFix} replacing the specified match (usually of another template)
* with this template.
*/
@Override
public Fix replace(ExpressionTemplateMatch match) {
Inliner inliner = match.createInliner();
int prec = getPrecedence(match.getLocation(), inliner.getContext());
SuggestedFix.Builder fix = SuggestedFix.builder();
try {
StringWriter writer = new StringWriter();
pretty(inliner.getContext(), writer).printExpr(expression().inline(inliner), prec);
fix.replace(match.getLocation(), writer.toString());
} catch (CouldNotResolveImportException e) {
logger.log(SEVERE, "Failure to resolve in replacement", e);
} catch (IOException e) {
throw new RuntimeException(e);
}
return addImports(inliner, fix);
}
/**
* Returns the precedence level appropriate for unambiguously printing
* leaf as a subexpression of its parent.
*/
private static int getPrecedence(JCTree leaf, Context context) {
JCCompilationUnit comp = context.get(JCCompilationUnit.class);
JCTree parent = TreeInfo.pathFor(leaf, comp).get(1);
// In general, this should match the logic in com.sun.tools.javac.tree.Pretty.
//
// TODO(mdempsky): There are probably cases where we could omit parentheses
// by tweaking the returned precedence, but they need careful review.
// For example, consider a template to replace "add(a, b)" with "a + b",
// which applied to "x + add(y, z)" would result in "x + (y + z)".
// In most cases, we'd likely prefer "x + y + z" instead, but those aren't
// always equivalent: "0L + (Integer.MIN_VALUE + Integer.MIN_VALUE)" yields
// a different value than "0L + Integer.MIN_VALUE + Integer.MIN_VALUE" due
// to integer promotion rules.
if (parent instanceof JCConditional) {
// This intentionally differs from Pretty, because Pretty appears buggy:
// http://mail.openjdk.java.net/pipermail/compiler-dev/2013-September/007303.html
JCConditional conditional = (JCConditional) parent;
return TreeInfo.condPrec + ((conditional.cond == leaf) ? 1 : 0);
} else if (parent instanceof JCAssign) {
JCAssign assign = (JCAssign) parent;
return TreeInfo.assignPrec + ((assign.lhs == leaf) ? 1 : 0);
} else if (parent instanceof JCAssignOp) {
JCAssignOp assignOp = (JCAssignOp) parent;
return TreeInfo.assignopPrec + ((assignOp.lhs == leaf) ? 1 : 0);
} else if (parent instanceof JCUnary) {
return TreeInfo.opPrec(parent.getTag());
} else if (parent instanceof JCBinary) {
JCBinary binary = (JCBinary) parent;
return TreeInfo.opPrec(parent.getTag()) + ((binary.rhs == leaf) ? 1 : 0);
} else if (parent instanceof JCTypeCast) {
JCTypeCast typeCast = (JCTypeCast) parent;
return (typeCast.expr == leaf) ? TreeInfo.prefixPrec : TreeInfo.noPrec;
} else if (parent instanceof JCInstanceOf) {
JCInstanceOf instanceOf = (JCInstanceOf) parent;
return TreeInfo.ordPrec + ((instanceOf.clazz == leaf) ? 1 : 0);
} else if (parent instanceof JCArrayAccess) {
JCArrayAccess arrayAccess = (JCArrayAccess) parent;
return (arrayAccess.indexed == leaf) ? TreeInfo.postfixPrec : TreeInfo.noPrec;
} else if (parent instanceof JCFieldAccess) {
JCFieldAccess fieldAccess = (JCFieldAccess) parent;
return (fieldAccess.selected == leaf) ? TreeInfo.postfixPrec : TreeInfo.noPrec;
} else {
return TreeInfo.noPrec;
}
}
}