blob: 28879edb1e40bce5d555d965d345890571b773a8 [file] [log] [blame]
package org.checkerframework.framework.util;
import com.github.javaparser.ParseProblemException;
import com.github.javaparser.ast.ArrayCreationLevel;
import com.github.javaparser.ast.expr.ArrayAccessExpr;
import com.github.javaparser.ast.expr.ArrayCreationExpr;
import com.github.javaparser.ast.expr.BinaryExpr;
import com.github.javaparser.ast.expr.BooleanLiteralExpr;
import com.github.javaparser.ast.expr.CharLiteralExpr;
import com.github.javaparser.ast.expr.ClassExpr;
import com.github.javaparser.ast.expr.DoubleLiteralExpr;
import com.github.javaparser.ast.expr.EnclosedExpr;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.FieldAccessExpr;
import com.github.javaparser.ast.expr.IntegerLiteralExpr;
import com.github.javaparser.ast.expr.LongLiteralExpr;
import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.ast.expr.NameExpr;
import com.github.javaparser.ast.expr.NullLiteralExpr;
import com.github.javaparser.ast.expr.StringLiteralExpr;
import com.github.javaparser.ast.expr.SuperExpr;
import com.github.javaparser.ast.expr.ThisExpr;
import com.github.javaparser.ast.expr.UnaryExpr;
import com.github.javaparser.ast.type.Type;
import com.github.javaparser.ast.visitor.GenericVisitorWithDefaults;
import com.sun.source.tree.Tree;
import com.sun.source.util.TreePath;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Symbol.PackageSymbol;
import com.sun.tools.javac.code.Type.ArrayType;
import com.sun.tools.javac.code.Type.ClassType;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic.Kind;
import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.dataflow.expression.ArrayAccess;
import org.checkerframework.dataflow.expression.ArrayCreation;
import org.checkerframework.dataflow.expression.BinaryOperation;
import org.checkerframework.dataflow.expression.ClassName;
import org.checkerframework.dataflow.expression.FieldAccess;
import org.checkerframework.dataflow.expression.FormalParameter;
import org.checkerframework.dataflow.expression.JavaExpression;
import org.checkerframework.dataflow.expression.LocalVariable;
import org.checkerframework.dataflow.expression.MethodCall;
import org.checkerframework.dataflow.expression.ThisReference;
import org.checkerframework.dataflow.expression.UnaryOperation;
import org.checkerframework.dataflow.expression.ValueLiteral;
import org.checkerframework.framework.source.DiagMessage;
import org.checkerframework.framework.util.dependenttypes.DependentTypesError;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.ElementUtils;
import org.checkerframework.javacutil.Resolver;
import org.checkerframework.javacutil.TypesUtils;
import org.checkerframework.javacutil.trees.TreeBuilder;
import org.plumelib.util.CollectionsPlume;
import org.plumelib.util.StringsPlume;
/**
* Helper methods to parse a string that represents a restricted Java expression.
*
* @checker_framework.manual #java-expressions-as-arguments Writing Java expressions as annotation
* arguments
* @checker_framework.manual #dependent-types Annotations whose argument is a Java expression
* (dependent type annotations)
*/
public class JavaExpressionParseUtil {
/** Regular expression for a formal parameter use. */
protected static final String PARAMETER_REGEX = "#([1-9][0-9]*)";
/**
* Anchored pattern for a formal parameter use; matches a string that is exactly a formal
* parameter use.
*/
protected static final Pattern ANCHORED_PARAMETER_PATTERN =
Pattern.compile("^" + PARAMETER_REGEX + "$");
/**
* Unanchored pattern for a formal parameter use; can be used to find all formal parameter uses.
*/
protected static final Pattern UNANCHORED_PARAMETER_PATTERN = Pattern.compile(PARAMETER_REGEX);
/**
* Parsable replacement for formal parameter references. It is parsable because it is a Java
* identifier.
*/
private static final String PARAMETER_PREFIX = "_param_";
/** The length of {@link #PARAMETER_PREFIX}. */
private static final int PARAMETER_PREFIX_LENGTH = PARAMETER_PREFIX.length();
/** A pattern that matches the start of a formal parameter in "#2" syntax. */
private static Pattern FORMAL_PARAMETER = Pattern.compile("#(\\d)");
/** The replacement for a formal parameter in "#2" syntax. */
private static final String PARAMETER_REPLACEMENT = PARAMETER_PREFIX + "$1";
/**
* Parses a string to a {@link JavaExpression}.
*
* <p>For most uses, clients should call one of the static methods in {@link
* StringToJavaExpression} rather than calling this method directly.
*
* @param expression the string expression to parse
* @param enclosingType type of the class that encloses the JavaExpression
* @param thisReference the JavaExpression to which to parse "this", or null if "this" should not
* appear in the expression
* @param parameters list of JavaExpressions to which to parse formal parameter references such as
* "#2", or null if formal parameter references should not appear in the expression
* @param localVarPath if non-null, the expression is parsed as if it were written at this
* location; affects only parsing of local variables
* @param pathToCompilationUnit required to use the underlying Javac API
* @param env the processing environment
* @return {@code expression} as a {@code JavaExpression}
* @throws JavaExpressionParseException if the string cannot be parsed
*/
public static JavaExpression parse(
String expression,
TypeMirror enclosingType,
@Nullable ThisReference thisReference,
@Nullable List<FormalParameter> parameters,
@Nullable TreePath localVarPath,
TreePath pathToCompilationUnit,
ProcessingEnvironment env)
throws JavaExpressionParseException {
String expressionWithParameterNames =
StringsPlume.replaceAll(expression, FORMAL_PARAMETER, PARAMETER_REPLACEMENT);
Expression expr;
try {
expr = JavaParserUtil.parseExpression(expressionWithParameterNames);
} catch (ParseProblemException e) {
String extra = ".";
if (!e.getProblems().isEmpty()) {
String message = e.getProblems().get(0).getMessage();
int newLine = message.indexOf(System.lineSeparator());
if (newLine != -1) {
message = message.substring(0, newLine);
}
extra = ". Error message: " + message;
}
throw constructJavaExpressionParseError(expression, "the expression did not parse" + extra);
}
JavaExpression result =
ExpressionToJavaExpressionVisitor.convert(
expr,
enclosingType,
thisReference,
parameters,
localVarPath,
pathToCompilationUnit,
env);
if (result instanceof ClassName && !expression.endsWith(".class")) {
throw constructJavaExpressionParseError(
expression,
String.format(
"a class name cannot terminate a Java expression string, where result=%s [%s]",
result, result.getClass()));
}
return result;
}
/**
* A visitor class that converts a JavaParser {@link Expression} to a {@link JavaExpression}. This
* class does not viewpoint-adapt the expression.
*/
private static class ExpressionToJavaExpressionVisitor
extends GenericVisitorWithDefaults<JavaExpression, Void> {
/**
* The underlying javac API used to convert from Strings to Elements requires a tree path even
* when the information could be deduced from elements alone. So use the path to the current
* CompilationUnit.
*/
private final TreePath pathToCompilationUnit;
/** If non-null, the expression is parsed as if it were written at this location. */
private final @Nullable TreePath localVarPath;
/** The processing environment. */
private final ProcessingEnvironment env;
/** The resolver. Computed from the environment, but lazily initialized. */
private @MonotonicNonNull Resolver resolver = null;
/** The type utilities. */
private final Types types;
/** The java.lang.String type. */
private final TypeMirror stringTypeMirror;
/** The enclosing type. Used to look up unqualified method, field, and class names. */
private final TypeMirror enclosingType;
/**
* The expression to use for "this". If {@code null}, a parse error will be thrown if "this"
* appears in the expression.
*/
private final @Nullable ThisReference thisReference;
/**
* For each formal parameter, the expression to which to parse it. For example, the second
* (index 1) element of the list is what "#2" parses to. If this field is {@code null}, a parse
* error will be thrown if "#2" appears in the expression.
*/
private final @Nullable List<FormalParameter> parameters;
/**
* Create a new ExpressionToJavaExpressionVisitor.
*
* @param enclosingType type of the class that encloses the JavaExpression
* @param thisReference JavaExpression to which to parse "this", or null if "this" should not
* appear in the expression
* @param parameters list of JavaExpressions to which to parse a formal parameter reference such
* as "#2", or null if parameters should not appear in the expression
* @param localVarPath if non-null, the expression is parsed as if it were written at this
* location
* @param pathToCompilationUnit required to use the underlying Javac API
* @param env the processing environment
*/
private ExpressionToJavaExpressionVisitor(
TypeMirror enclosingType,
@Nullable ThisReference thisReference,
@Nullable List<FormalParameter> parameters,
@Nullable TreePath localVarPath,
TreePath pathToCompilationUnit,
ProcessingEnvironment env) {
this.pathToCompilationUnit = pathToCompilationUnit;
this.localVarPath = localVarPath;
this.env = env;
this.types = env.getTypeUtils();
this.stringTypeMirror = env.getElementUtils().getTypeElement("java.lang.String").asType();
this.enclosingType = enclosingType;
this.thisReference = thisReference;
this.parameters = parameters;
}
/**
* Converts a JavaParser {@link Expression} to a {@link JavaExpression}.
*
* @param expr the JavaParser {@link Expression} to convert
* @param enclosingType type of the class that encloses the JavaExpression
* @param thisReference JavaExpression to which to parse "this", or null if "this" should not
* appear in the expression
* @param parameters list of JavaExpressions to which to parse parameters, or null if parameters
* should not appear in the expression
* @param localVarPath if non-null, the expression is parsed as if it were written at this
* location
* @param pathToCompilationUnit required to use the underlying Javac API
* @param env the processing environment
* @return {@code expr} as a {@code JavaExpression}
* @throws JavaExpressionParseException if {@code expr} cannot be converted to a {@code
* JavaExpression}
*/
public static JavaExpression convert(
Expression expr,
TypeMirror enclosingType,
@Nullable ThisReference thisReference,
@Nullable List<FormalParameter> parameters,
@Nullable TreePath localVarPath,
TreePath pathToCompilationUnit,
ProcessingEnvironment env)
throws JavaExpressionParseException {
try {
return expr.accept(
new ExpressionToJavaExpressionVisitor(
enclosingType, thisReference, parameters, localVarPath, pathToCompilationUnit, env),
null);
} catch (ParseRuntimeException e) {
// Convert unchecked to checked exception. Visitor methods can't throw checked
// exceptions. They override the methods in the superclass, and a checked exception
// would change the method signature.
throw e.getCheckedException();
}
}
/**
* Initializes the {@code resolver} field if necessary. Does nothing on invocations after the
* first.
*/
private void setResolverField() {
if (resolver == null) {
resolver = new Resolver(env);
}
}
/** If the expression is not supported, throw a {@link ParseRuntimeException} by default. */
@Override
public JavaExpression defaultAction(com.github.javaparser.ast.Node n, Void aVoid) {
throw new ParseRuntimeException(
constructJavaExpressionParseError(
n.toString(), n.getClass() + " is not a supported expression"));
}
@Override
public JavaExpression visit(NullLiteralExpr expr, Void aVoid) {
return new ValueLiteral(types.getNullType(), (Object) null);
}
@Override
public JavaExpression visit(IntegerLiteralExpr expr, Void aVoid) {
return new ValueLiteral(types.getPrimitiveType(TypeKind.INT), expr.asNumber());
}
@Override
public JavaExpression visit(LongLiteralExpr expr, Void aVoid) {
return new ValueLiteral(types.getPrimitiveType(TypeKind.LONG), expr.asNumber());
}
@Override
public JavaExpression visit(CharLiteralExpr expr, Void aVoid) {
return new ValueLiteral(types.getPrimitiveType(TypeKind.CHAR), expr.asChar());
}
@Override
public JavaExpression visit(DoubleLiteralExpr expr, Void aVoid) {
return new ValueLiteral(types.getPrimitiveType(TypeKind.DOUBLE), expr.asDouble());
}
@Override
public JavaExpression visit(StringLiteralExpr expr, Void aVoid) {
return new ValueLiteral(stringTypeMirror, expr.asString());
}
@Override
public JavaExpression visit(BooleanLiteralExpr expr, Void aVoid) {
return new ValueLiteral(types.getPrimitiveType(TypeKind.BOOLEAN), expr.getValue());
}
@Override
public JavaExpression visit(ThisExpr n, Void aVoid) {
if (thisReference == null) {
throw new ParseRuntimeException(
constructJavaExpressionParseError("this", "\"this\" isn't allowed here"));
}
return thisReference;
}
@Override
public JavaExpression visit(SuperExpr n, Void aVoid) {
// super literal
TypeMirror superclass = TypesUtils.getSuperclass(enclosingType, types);
if (superclass == null) {
throw new ParseRuntimeException(
constructJavaExpressionParseError("super", enclosingType + " has no superclass"));
}
return new ThisReference(superclass);
}
// expr is an expression in parentheses.
@Override
public JavaExpression visit(EnclosedExpr expr, Void aVoid) {
return expr.getInner().accept(this, null);
}
@Override
public JavaExpression visit(ArrayAccessExpr expr, Void aVoid) {
JavaExpression array = expr.getName().accept(this, null);
TypeMirror arrayType = array.getType();
if (arrayType.getKind() != TypeKind.ARRAY) {
throw new ParseRuntimeException(
constructJavaExpressionParseError(
expr.toString(),
String.format(
"expected an array, found %s of type %s [%s]",
array, arrayType, arrayType.getKind())));
}
TypeMirror componentType = ((ArrayType) arrayType).getComponentType();
JavaExpression index = expr.getIndex().accept(this, null);
return new ArrayAccess(componentType, array, index);
}
// expr is an identifier with no dots in its name.
@Override
public JavaExpression visit(NameExpr expr, Void aVoid) {
String s = expr.getNameAsString();
setResolverField();
// Formal parameter, using "#2" syntax.
JavaExpression parameter = getParameterJavaExpression(s);
if (parameter != null) {
// A parameter is a local variable, but it can be referenced outside of local scope
// (at the method scope) using the special #NN syntax.
return parameter;
}
// Local variable or parameter.
if (localVarPath != null) {
// Attempt to match a local variable within the scope of the
// given path before attempting to match a field.
VariableElement varElem = resolver.findLocalVariableOrParameter(s, localVarPath);
if (varElem != null) {
return new LocalVariable(varElem);
}
}
// Field access
JavaExpression fieldAccessReceiver;
if (thisReference != null) {
fieldAccessReceiver = thisReference;
} else {
fieldAccessReceiver = new ClassName(enclosingType);
}
FieldAccess fieldAccess = getIdentifierAsFieldAccess(fieldAccessReceiver, s);
if (fieldAccess != null) {
return fieldAccess;
}
if (localVarPath != null) {
Element classElem = resolver.findClass(s, localVarPath);
TypeMirror classType = ElementUtils.getType(classElem);
if (classType != null) {
return new ClassName(classType);
}
}
ClassName classType = getIdentifierAsUnqualifiedClassName(s);
if (classType != null) {
return classType;
}
// Err if a formal parameter name is used, instead of the "#2" syntax.
if (parameters != null) {
for (int i = 0; i < parameters.size(); i++) {
Element varElt = parameters.get(i).getElement();
if (varElt.getSimpleName().contentEquals(s)) {
throw new ParseRuntimeException(
constructJavaExpressionParseError(
s, String.format(DependentTypesError.FORMAL_PARAM_NAME_STRING, i + 1, s)));
}
}
}
throw new ParseRuntimeException(constructJavaExpressionParseError(s, "identifier not found"));
}
/**
* If {@code s} a parameter expressed using the {@code #NN} syntax, then returns a
* JavaExpression for the given parameter; that is, returns an element of {@code parameters}.
* Otherwise, returns {@code null}.
*
* @param s a String that starts with PARAMETER_PREFIX
* @return the JavaExpression for the given parameter or {@code null} if {@code s} is not a
* parameter
*/
private @Nullable JavaExpression getParameterJavaExpression(String s) {
if (!s.startsWith(PARAMETER_PREFIX)) {
return null;
}
if (parameters == null) {
throw new ParseRuntimeException(
constructJavaExpressionParseError(s, "no parameters found"));
}
int idx = Integer.parseInt(s.substring(PARAMETER_PREFIX_LENGTH));
if (idx == 0) {
throw new ParseRuntimeException(
constructJavaExpressionParseError(
"#0", "Use \"this\" for the receiver or \"#1\" for the first formal parameter"));
}
if (idx > parameters.size()) {
throw new ParseRuntimeException(
new JavaExpressionParseException(
"flowexpr.parse.index.too.big", Integer.toString(idx)));
}
return parameters.get(idx - 1);
}
/**
* If {@code identifier} is the simple class name of any inner class of {@code type}, return the
* {@link ClassName} for the inner class. If not, return null.
*
* @param type type to search for {@code identifier}
* @param identifier possible simple class name
* @return the {@code ClassName} for {@code identifier}, or null if it is not a simple class
* name
*/
protected @Nullable ClassName getIdentifierAsInnerClassName(
TypeMirror type, String identifier) {
if (type.getKind() != TypeKind.DECLARED) {
return null;
}
Element outerClass = ((DeclaredType) type).asElement();
for (Element memberElement : outerClass.getEnclosedElements()) {
if (!(memberElement.getKind().isClass() || memberElement.getKind().isInterface())) {
continue;
}
if (memberElement.getSimpleName().contentEquals(identifier)) {
return new ClassName(ElementUtils.getType(memberElement));
}
}
return null;
}
/**
* If {@code identifier} is a class name with that can be referenced using only its simple name
* within {@code enclosingType}, return the {@link ClassName} for the class. If not, return
* null.
*
* <p>{@code identifier} may be
*
* <ol>
* <li>the simple name of {@code type}.
* <li>the simple name of a class declared in {@code type} or in an enclosing type of {@code
* type}.
* <li>the simple name of a class in the java.lang package.
* <li>the simple name of a class in the unnamed package.
* </ol>
*
* @param identifier possible class name
* @return the {@code ClassName} for {@code identifier}, or null if it is not a class name
*/
protected @Nullable ClassName getIdentifierAsUnqualifiedClassName(String identifier) {
// Is identifier an inner class of enclosingType or of any enclosing class of enclosingType?
TypeMirror searchType = enclosingType;
while (searchType.getKind() == TypeKind.DECLARED) {
DeclaredType searchDeclaredType = (DeclaredType) searchType;
if (searchDeclaredType.asElement().getSimpleName().contentEquals(identifier)) {
return new ClassName(searchType);
}
ClassName className = getIdentifierAsInnerClassName(searchType, identifier);
if (className != null) {
return className;
}
searchType = getTypeOfEnclosingClass(searchDeclaredType);
}
if (enclosingType.getKind() == TypeKind.DECLARED) {
// Is identifier in the same package as this?
PackageSymbol packageSymbol =
(PackageSymbol)
ElementUtils.enclosingPackage(((DeclaredType) enclosingType).asElement());
ClassSymbol classSymbol =
resolver.findClassInPackage(identifier, packageSymbol, pathToCompilationUnit);
if (classSymbol != null) {
return new ClassName(classSymbol.asType());
}
}
// Is identifier a simple name for a class in java.lang?
Symbol.PackageSymbol packageSymbol = resolver.findPackage("java.lang", pathToCompilationUnit);
if (packageSymbol == null) {
throw new BugInCF("Can't find java.lang package.");
}
ClassSymbol classSymbol =
resolver.findClassInPackage(identifier, packageSymbol, pathToCompilationUnit);
if (classSymbol != null) {
return new ClassName(classSymbol.asType());
}
// Is identifier a class in the unnamed package?
Element classElem = resolver.findClass(identifier, pathToCompilationUnit);
if (classElem != null) {
PackageElement pkg = ElementUtils.enclosingPackage(classElem);
if (pkg != null && pkg.isUnnamed()) {
TypeMirror classType = ElementUtils.getType(classElem);
if (classType != null) {
return new ClassName(classType);
}
}
}
return null;
}
/**
* Return the {@link FieldAccess} expression for the field with name {@code identifier} accessed
* via {@code receiverExpr}. If no such field exists, then {@code null} is returned.
*
* @param receiverExpr the receiver of the field access; the expression used to access the field
* @param identifier possibly a field name
* @return a field access, or null if {@code identifier} is not a field that can be accessed via
* {@code receiverExpr}
*/
protected @Nullable FieldAccess getIdentifierAsFieldAccess(
JavaExpression receiverExpr, String identifier) {
// Find the field element.
TypeMirror enclosingTypeOfField = receiverExpr.getType();
VariableElement fieldElem;
if (identifier.equals("length") && enclosingTypeOfField.getKind() == TypeKind.ARRAY) {
fieldElem = resolver.findField(identifier, enclosingTypeOfField, pathToCompilationUnit);
if (fieldElem == null) {
throw new BugInCF("length field not found for type %s", enclosingTypeOfField);
}
} else {
fieldElem = null;
// Search for field in each enclosing class.
while (enclosingTypeOfField.getKind() == TypeKind.DECLARED) {
fieldElem = resolver.findField(identifier, enclosingTypeOfField, pathToCompilationUnit);
if (fieldElem != null) {
break;
}
enclosingTypeOfField = getTypeOfEnclosingClass((DeclaredType) enclosingTypeOfField);
}
if (fieldElem == null) {
// field not found.
return null;
}
}
// Construct a FieldAccess expression.
if (ElementUtils.isStatic(fieldElem)) {
Element classElem = fieldElem.getEnclosingElement();
JavaExpression staticClassReceiver = new ClassName(ElementUtils.getType(classElem));
return new FieldAccess(staticClassReceiver, fieldElem);
}
// fieldElem is an instance field.
if (receiverExpr instanceof ClassName) {
throw new ParseRuntimeException(
constructJavaExpressionParseError(
fieldElem.getSimpleName().toString(),
"a non-static field cannot have a class name as a receiver."));
}
// There are two possibilities, captured by local variable fieldDeclaredInReceiverType:
// * true: it's an instance field declared in the type (or supertype) of receiverExpr.
// * false: it's an instance field declared in an enclosing type of receiverExpr.
@SuppressWarnings("interning:not.interned") // Checking for exact object
boolean fieldDeclaredInReceiverType = enclosingTypeOfField == receiverExpr.getType();
if (fieldDeclaredInReceiverType) {
TypeMirror fieldType = ElementUtils.getType(fieldElem);
return new FieldAccess(receiverExpr, fieldType, fieldElem);
} else {
if (!(receiverExpr instanceof ThisReference)) {
String msg =
String.format(
"%s is declared in an outer type of the type of the receiver expression, %s.",
identifier, receiverExpr);
throw new ParseRuntimeException(constructJavaExpressionParseError(identifier, msg));
}
TypeElement receiverTypeElement = TypesUtils.getTypeElement(receiverExpr.getType());
if (receiverTypeElement == null || ElementUtils.isStatic(receiverTypeElement)) {
String msg =
String.format("%s is a non-static field declared in an outer type this.", identifier);
throw new ParseRuntimeException(constructJavaExpressionParseError(identifier, msg));
}
JavaExpression locationOfField = new ThisReference(enclosingTypeOfField);
return new FieldAccess(locationOfField, fieldElem);
}
}
@Override
public JavaExpression visit(MethodCallExpr expr, Void aVoid) {
setResolverField();
JavaExpression receiverExpr;
if (expr.getScope().isPresent()) {
receiverExpr = expr.getScope().get().accept(this, null);
expr = expr.removeScope();
} else if (thisReference != null) {
receiverExpr = thisReference;
} else {
receiverExpr = new ClassName(enclosingType);
}
String methodName = expr.getNameAsString();
// parse argument list
List<JavaExpression> arguments =
CollectionsPlume.mapList(argument -> argument.accept(this, null), expr.getArguments());
ExecutableElement methodElement;
try {
methodElement =
getMethodElement(
methodName, receiverExpr.getType(), pathToCompilationUnit, arguments, resolver);
} catch (JavaExpressionParseException e) {
throw new ParseRuntimeException(e);
}
// Box any arguments that require it.
for (int i = 0; i < arguments.size(); i++) {
VariableElement parameter = methodElement.getParameters().get(i);
TypeMirror parameterType = parameter.asType();
JavaExpression argument = arguments.get(i);
TypeMirror argumentType = argument.getType();
// is boxing necessary?
if (TypesUtils.isBoxedPrimitive(parameterType) && TypesUtils.isPrimitive(argumentType)) {
// boxing is necessary
MethodSymbol valueOfMethod = TreeBuilder.getValueOfMethod(env, parameterType);
JavaExpression boxedParam =
new MethodCall(
parameterType,
valueOfMethod,
new ClassName(parameterType),
Collections.singletonList(argument));
arguments.set(i, boxedParam);
}
}
// Build the MethodCall expression object.
if (ElementUtils.isStatic(methodElement)) {
Element classElem = methodElement.getEnclosingElement();
JavaExpression staticClassReceiver = new ClassName(ElementUtils.getType(classElem));
return new MethodCall(
ElementUtils.getType(methodElement), methodElement, staticClassReceiver, arguments);
} else {
if (receiverExpr instanceof ClassName) {
throw new ParseRuntimeException(
constructJavaExpressionParseError(
expr.toString(),
"a non-static method call cannot have a class name as a receiver"));
}
TypeMirror methodType =
TypesUtils.substituteMethodReturnType(methodElement, receiverExpr.getType(), env);
return new MethodCall(methodType, methodElement, receiverExpr, arguments);
}
}
/**
* Returns the ExecutableElement for a method, or throws an exception.
*
* <p>(This method takes into account autoboxing.)
*
* @param methodName the method name
* @param receiverType the receiver type
* @param pathToCompilationUnit the path to the compilation unit
* @param arguments the arguments
* @param resolver the resolver
* @return the ExecutableElement for a method, or throws an exception
* @throws JavaExpressionParseException if the string cannot be parsed as a method name
*/
private ExecutableElement getMethodElement(
String methodName,
TypeMirror receiverType,
TreePath pathToCompilationUnit,
List<JavaExpression> arguments,
Resolver resolver)
throws JavaExpressionParseException {
List<TypeMirror> argumentTypes = CollectionsPlume.mapList(JavaExpression::getType, arguments);
if (receiverType.getKind() == TypeKind.ARRAY) {
ExecutableElement element =
resolver.findMethod(methodName, receiverType, pathToCompilationUnit, argumentTypes);
if (element == null) {
throw constructJavaExpressionParseError(methodName, "no such method");
}
return element;
}
// Search for method in each enclosing class.
while (receiverType.getKind() == TypeKind.DECLARED) {
ExecutableElement element =
resolver.findMethod(methodName, receiverType, pathToCompilationUnit, argumentTypes);
if (element != null) {
return element;
}
receiverType = getTypeOfEnclosingClass((DeclaredType) receiverType);
}
// Method not found.
throw constructJavaExpressionParseError(methodName, "no such method");
}
// expr is a field access, a fully qualified class name, or a class name qualified with
// another class name (e.g. {@code OuterClass.InnerClass})
@Override
public JavaExpression visit(FieldAccessExpr expr, Void aVoid) {
setResolverField();
Expression scope = expr.getScope();
String name = expr.getNameAsString();
// Check for fully qualified class name.
Symbol.PackageSymbol packageSymbol =
resolver.findPackage(scope.toString(), pathToCompilationUnit);
if (packageSymbol != null) {
ClassSymbol classSymbol =
resolver.findClassInPackage(name, packageSymbol, pathToCompilationUnit);
if (classSymbol != null) {
return new ClassName(classSymbol.asType());
}
throw new ParseRuntimeException(
constructJavaExpressionParseError(
expr.toString(),
"could not find class "
+ expr.getNameAsString()
+ " in package "
+ scope.toString()));
}
JavaExpression receiver = scope.accept(this, null);
// Check for field access expression.
FieldAccess fieldAccess = getIdentifierAsFieldAccess(receiver, name);
if (fieldAccess != null) {
return fieldAccess;
}
// Check for inner class.
ClassName classType = getIdentifierAsInnerClassName(receiver.getType(), name);
if (classType != null) {
return classType;
}
throw new ParseRuntimeException(
constructJavaExpressionParseError(
name, String.format("field or class %s not found in %s", name, receiver)));
}
// expr is a Class literal
@Override
public JavaExpression visit(ClassExpr expr, Void aVoid) {
TypeMirror result = convertTypeToTypeMirror(expr.getType());
if (result == null) {
throw new ParseRuntimeException(
constructJavaExpressionParseError(
expr.toString(), "it is an unparsable class literal"));
}
return new ClassName(result);
}
@Override
public JavaExpression visit(ArrayCreationExpr expr, Void aVoid) {
List<JavaExpression> dimensions =
CollectionsPlume.mapList(
(ArrayCreationLevel dimension) ->
dimension.getDimension().isPresent()
? dimension.getDimension().get().accept(this, aVoid)
: null,
expr.getLevels());
List<JavaExpression> initializers;
if (expr.getInitializer().isPresent()) {
initializers =
CollectionsPlume.mapList(
(Expression initializer) -> initializer.accept(this, null),
expr.getInitializer().get().getValues());
} else {
initializers = Collections.emptyList();
}
TypeMirror arrayType = convertTypeToTypeMirror(expr.getElementType());
if (arrayType == null) {
throw new ParseRuntimeException(
constructJavaExpressionParseError(
expr.getElementType().asString(), "type not parsable"));
}
for (int i = 0; i < dimensions.size(); i++) {
arrayType = TypesUtils.createArrayType(arrayType, env.getTypeUtils());
}
return new ArrayCreation(arrayType, dimensions, initializers);
}
@Override
public JavaExpression visit(UnaryExpr expr, Void aVoid) {
Tree.Kind treeKind = javaParserUnaryOperatorToTreeKind(expr.getOperator());
JavaExpression operand = expr.getExpression().accept(this, null);
// This eliminates + and performs constant-folding for -; it could also do so for other
// operations.
switch (treeKind) {
case UNARY_PLUS:
return operand;
case UNARY_MINUS:
if (operand instanceof ValueLiteral) {
return ((ValueLiteral) operand).negate();
}
break;
default:
// Not optimization for this operand
break;
}
return new UnaryOperation(operand.getType(), treeKind, operand);
}
/**
* Convert a JavaParser unary operator to a TreeKind.
*
* @param op a JavaParser unary operator
* @return a TreeKind for the unary operator
*/
private Tree.Kind javaParserUnaryOperatorToTreeKind(UnaryExpr.Operator op) {
switch (op) {
case BITWISE_COMPLEMENT:
return Tree.Kind.BITWISE_COMPLEMENT;
case LOGICAL_COMPLEMENT:
return Tree.Kind.LOGICAL_COMPLEMENT;
case MINUS:
return Tree.Kind.UNARY_MINUS;
case PLUS:
return Tree.Kind.UNARY_PLUS;
case POSTFIX_DECREMENT:
return Tree.Kind.POSTFIX_DECREMENT;
case POSTFIX_INCREMENT:
return Tree.Kind.POSTFIX_INCREMENT;
case PREFIX_DECREMENT:
return Tree.Kind.PREFIX_DECREMENT;
case PREFIX_INCREMENT:
return Tree.Kind.PREFIX_INCREMENT;
default:
throw new BugInCF("unhandled " + op);
}
}
@Override
public JavaExpression visit(BinaryExpr expr, Void aVoid) {
JavaExpression leftJe = expr.getLeft().accept(this, null);
JavaExpression rightJe = expr.getRight().accept(this, null);
TypeMirror leftType = leftJe.getType();
TypeMirror rightType = rightJe.getType();
TypeMirror type;
// isSubtype() first does the cheaper test isSameType(), so no need to do it here.
if (types.isSubtype(leftType, rightType)) {
type = rightType;
} else if (types.isSubtype(rightType, leftType)) {
type = leftType;
} else if (expr.getOperator() == BinaryExpr.Operator.PLUS
&& (TypesUtils.isString(leftType) || TypesUtils.isString(rightType))) {
type = stringTypeMirror;
} else {
throw new ParseRuntimeException(
constructJavaExpressionParseError(
expr.toString(),
String.format("inconsistent types %s %s for %s", leftType, rightType, expr)));
}
return new BinaryOperation(
type, javaParserBinaryOperatorToTreeKind(expr.getOperator()), leftJe, rightJe);
}
/**
* Convert a JavaParser binary operator to a TreeKind.
*
* @param op a JavaParser binary operator
* @return a TreeKind for the binary operator
*/
private Tree.Kind javaParserBinaryOperatorToTreeKind(BinaryExpr.Operator op) {
switch (op) {
case AND:
return Tree.Kind.CONDITIONAL_AND;
case BINARY_AND:
return Tree.Kind.AND;
case BINARY_OR:
return Tree.Kind.OR;
case DIVIDE:
return Tree.Kind.DIVIDE;
case EQUALS:
return Tree.Kind.EQUAL_TO;
case GREATER:
return Tree.Kind.GREATER_THAN;
case GREATER_EQUALS:
return Tree.Kind.GREATER_THAN_EQUAL;
case LEFT_SHIFT:
return Tree.Kind.LEFT_SHIFT;
case LESS:
return Tree.Kind.LESS_THAN;
case LESS_EQUALS:
return Tree.Kind.LESS_THAN_EQUAL;
case MINUS:
return Tree.Kind.MINUS;
case MULTIPLY:
return Tree.Kind.MULTIPLY;
case NOT_EQUALS:
return Tree.Kind.NOT_EQUAL_TO;
case OR:
return Tree.Kind.CONDITIONAL_OR;
case PLUS:
return Tree.Kind.PLUS;
case REMAINDER:
return Tree.Kind.REMAINDER;
case SIGNED_RIGHT_SHIFT:
return Tree.Kind.RIGHT_SHIFT;
case UNSIGNED_RIGHT_SHIFT:
return Tree.Kind.UNSIGNED_RIGHT_SHIFT;
case XOR:
return Tree.Kind.XOR;
default:
throw new Error("unhandled " + op);
}
}
/**
* Converts the JavaParser type to a TypeMirror. Returns null if {@code type} is not handled;
* this method does not handle type variables, union types, or intersection types.
*
* @param type a JavaParser type
* @return a TypeMirror corresponding to {@code type}, or null if {@code type} isn't handled
*/
private @Nullable TypeMirror convertTypeToTypeMirror(Type type) {
if (type.isClassOrInterfaceType()) {
try {
return JavaParserUtil.parseExpression(type.asString()).accept(this, null).getType();
} catch (ParseProblemException e) {
return null;
}
} else if (type.isPrimitiveType()) {
switch (type.asPrimitiveType().getType()) {
case BOOLEAN:
return types.getPrimitiveType(TypeKind.BOOLEAN);
case BYTE:
return types.getPrimitiveType(TypeKind.BYTE);
case SHORT:
return types.getPrimitiveType(TypeKind.SHORT);
case INT:
return types.getPrimitiveType(TypeKind.INT);
case CHAR:
return types.getPrimitiveType(TypeKind.CHAR);
case FLOAT:
return types.getPrimitiveType(TypeKind.FLOAT);
case LONG:
return types.getPrimitiveType(TypeKind.LONG);
case DOUBLE:
return types.getPrimitiveType(TypeKind.DOUBLE);
}
} else if (type.isVoidType()) {
return types.getNoType(TypeKind.VOID);
} else if (type.isArrayType()) {
TypeMirror componentType = convertTypeToTypeMirror(type.asArrayType().getComponentType());
if (componentType == null) {
return null;
}
return types.getArrayType(componentType);
}
return null;
}
}
/**
* If {@code s} is exactly a formal parameter, return its 1-based index. Returns -1 otherwise.
*
* @param s a Java expression
* @return the 1-based index of the formal parameter that {@code s} represents, or -1
*/
public static int parameterIndex(String s) {
Matcher matcher = ANCHORED_PARAMETER_PATTERN.matcher(s);
if (matcher.find()) {
return Integer.parseInt(matcher.group(1));
}
return -1;
}
///////////////////////////////////////////////////////////////////////////
/// Contexts
///
/**
* Returns the type of the innermost enclosing class. Returns Type.noType if the type is a
* top-level class.
*
* <p>If the innermost enclosing class is static, this method returns the type of that class. By
* contrast, {@link DeclaredType#getEnclosingType()} returns the type of the innermost enclosing
* class that is not static.
*
* @param type a DeclaredType
* @return the type of the innermost enclosing class or Type.noType
*/
private static TypeMirror getTypeOfEnclosingClass(DeclaredType type) {
if (type instanceof ClassType) {
// enclClass() needs to be called on tsym.owner, because tsym.enclClass() == tsym.
Symbol sym = ((ClassType) type).tsym.owner;
if (sym == null) {
return com.sun.tools.javac.code.Type.noType;
}
ClassSymbol cs = sym.enclClass();
if (cs == null) {
return com.sun.tools.javac.code.Type.noType;
}
return cs.asType();
} else {
return type.getEnclosingType();
}
}
///////////////////////////////////////////////////////////////////////////
/// Exceptions
///
/**
* An exception that indicates a parse error. Call {@link #getDiagMessage} to obtain a {@link
* DiagMessage} that can be used for error reporting.
*/
public static class JavaExpressionParseException extends Exception {
private static final long serialVersionUID = 2L;
/** The error message key. */
private @CompilerMessageKey String errorKey;
/** The arguments to the error message key. */
public final Object[] args;
/**
* Create a new JavaExpressionParseException.
*
* @param errorKey the error message key
* @param args the arguments to the error message key
*/
public JavaExpressionParseException(@CompilerMessageKey String errorKey, Object... args) {
this(null, errorKey, args);
}
/**
* Create a new JavaExpressionParseException.
*
* @param cause cause
* @param errorKey the error message key
* @param args the arguments to the error message key
*/
public JavaExpressionParseException(
@Nullable Throwable cause, @CompilerMessageKey String errorKey, Object... args) {
super(cause);
this.errorKey = errorKey;
this.args = args;
}
@Override
public String getMessage() {
return errorKey + " " + Arrays.toString(args);
}
/**
* Return a DiagMessage that can be used for error reporting.
*
* @return a DiagMessage that can be used for error reporting
*/
public DiagMessage getDiagMessage() {
return new DiagMessage(Kind.ERROR, errorKey, args);
}
public boolean isFlowParseError() {
return errorKey.endsWith("flowexpr.parse.error");
}
}
/**
* Returns a {@link JavaExpressionParseException} with error key "flowexpr.parse.error" for the
* expression {@code expr} with explanation {@code explanation}.
*
* @param expr the string that could not be parsed
* @param explanation an explanation of the parse failure
* @return a {@link JavaExpressionParseException} for the expression {@code expr} with explanation
* {@code explanation}.
*/
private static JavaExpressionParseException constructJavaExpressionParseError(
String expr, String explanation) {
if (expr == null) {
throw new BugInCF("Must have an expression.");
}
if (explanation == null) {
throw new BugInCF("Must have an explanation.");
}
return new JavaExpressionParseException(
(Throwable) null, "flowexpr.parse.error", "Invalid '" + expr + "' because " + explanation);
}
/**
* The unchecked exception equivalent of checked exception {@link JavaExpressionParseException}.
*/
private static class ParseRuntimeException extends RuntimeException {
private static final long serialVersionUID = 2L;
private final JavaExpressionParseException exception;
private ParseRuntimeException(JavaExpressionParseException exception) {
this.exception = exception;
}
private JavaExpressionParseException getCheckedException() {
return exception;
}
}
}