blob: ec59807eab09914bac38c83ae1a4d06168be42b1 [file] [log] [blame]
package org.checkerframework.javacutil;
import com.sun.source.util.TreePath;
import com.sun.source.util.Trees;
import com.sun.tools.javac.api.JavacScope;
import com.sun.tools.javac.code.Kinds;
import com.sun.tools.javac.code.Kinds.KindSelector;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.code.Symbol.PackageSymbol;
import com.sun.tools.javac.code.Symbol.TypeSymbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.comp.AttrContext;
import com.sun.tools.javac.comp.DeferredAttr;
import com.sun.tools.javac.comp.Env;
import com.sun.tools.javac.comp.Resolve;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.Log;
import com.sun.tools.javac.util.Name;
import com.sun.tools.javac.util.Names;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
/** A utility class to find symbols corresponding to string references (identifiers). */
// This class reflectively accesses jdk.compiler/com.sun.tools.javac.comp.
// This is why --add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED is required when
// running the Checker Framework. If this class is re-written, then that --add-opens should be
// removed.
public class Resolver {
private final Resolve resolve;
private final Names names;
private final Trees trees;
private final Log log;
private static final Method FIND_METHOD;
private static final Method FIND_VAR;
private static final Method FIND_IDENT;
private static final Method FIND_IDENT_IN_TYPE;
private static final Method FIND_IDENT_IN_PACKAGE;
private static final Method FIND_TYPE;
private static final Class<?> ACCESSERROR;
// Note that currently access(...) is defined in InvalidSymbolError, a superclass of AccessError
private static final Method ACCESSERROR_ACCESS;
static {
try {
FIND_METHOD =
Resolve.class.getDeclaredMethod(
"findMethod",
Env.class,
Type.class,
Name.class,
List.class,
List.class,
boolean.class,
boolean.class);
FIND_METHOD.setAccessible(true);
FIND_VAR = Resolve.class.getDeclaredMethod("findVar", Env.class, Name.class);
FIND_VAR.setAccessible(true);
FIND_IDENT =
Resolve.class.getDeclaredMethod("findIdent", Env.class, Name.class, KindSelector.class);
FIND_IDENT.setAccessible(true);
FIND_IDENT_IN_TYPE =
Resolve.class.getDeclaredMethod(
"findIdentInType", Env.class, Type.class, Name.class, KindSelector.class);
FIND_IDENT_IN_TYPE.setAccessible(true);
FIND_IDENT_IN_PACKAGE =
Resolve.class.getDeclaredMethod(
"findIdentInPackage", Env.class, TypeSymbol.class, Name.class, KindSelector.class);
FIND_IDENT_IN_PACKAGE.setAccessible(true);
FIND_TYPE = Resolve.class.getDeclaredMethod("findType", Env.class, Name.class);
FIND_TYPE.setAccessible(true);
} catch (Exception e) {
Error err =
new AssertionError("Compiler 'Resolve' class doesn't contain required 'find' method");
err.initCause(e);
throw err;
}
try {
ACCESSERROR = Class.forName("com.sun.tools.javac.comp.Resolve$AccessError");
ACCESSERROR_ACCESS = ACCESSERROR.getMethod("access", Name.class, TypeSymbol.class);
ACCESSERROR_ACCESS.setAccessible(true);
} catch (ClassNotFoundException e) {
throw new BugInCF("Compiler 'Resolve$AccessError' class could not be retrieved.", e);
} catch (NoSuchMethodException e) {
throw new BugInCF(
"Compiler 'Resolve$AccessError' class doesn't contain required 'access' method", e);
}
}
public Resolver(ProcessingEnvironment env) {
Context context = ((JavacProcessingEnvironment) env).getContext();
this.resolve = Resolve.instance(context);
this.names = Names.instance(context);
this.trees = Trees.instance(env);
this.log = Log.instance(context);
}
/**
* Determine the environment for the given path.
*
* @param path the tree path to the local scope
* @return the corresponding attribution environment
*/
public Env<AttrContext> getEnvForPath(TreePath path) {
TreePath iter = path;
JavacScope scope = null;
while (scope == null && iter != null) {
try {
scope = (JavacScope) trees.getScope(iter);
} catch (Throwable t) {
// Work around Issue #1059 by skipping through the TreePath until something
// doesn't crash. This probably returns the class scope, so users might not
// get the variables they expect. But that is better than crashing.
iter = iter.getParentPath();
}
}
if (scope != null) {
return scope.getEnv();
} else {
throw new BugInCF("Could not determine any possible scope for path: " + path.getLeaf());
}
}
/**
* Finds the package with name {@code name}.
*
* @param name the name of the package
* @param path the tree path to the local scope
* @return the {@code PackageSymbol} for the package if it is found, {@code null} otherwise
*/
public @Nullable PackageSymbol findPackage(String name, TreePath path) {
Log.DiagnosticHandler discardDiagnosticHandler = new Log.DiscardDiagnosticHandler(log);
try {
Env<AttrContext> env = getEnvForPath(path);
Element res =
wrapInvocationOnResolveInstance(
FIND_IDENT, env, names.fromString(name), Kinds.KindSelector.PCK);
// findIdent will return a PackageSymbol even for a symbol that is not a package, such as
// a.b.c.MyClass.myStaticField. "exists()" must be called on it to ensure that it exists.
if (res.getKind() == ElementKind.PACKAGE) {
PackageSymbol ps = (PackageSymbol) res;
return ps.exists() ? ps : null;
} else {
return null;
}
} finally {
log.popDiagnosticHandler(discardDiagnosticHandler);
}
}
/**
* Finds the field with name {@code name} in {@code type} or a superclass or superinterface of
* {@code type}.
*
* <p>The method adheres to all the rules of Java's scoping (while also considering the imports)
* for name resolution.
*
* @param name the name of the field
* @param type the type of the receiver (i.e., the type in which to look for the field)
* @param path the tree path to the local scope
* @return the element for the field, {@code null} otherwise
*/
public @Nullable VariableElement findField(String name, TypeMirror type, TreePath path) {
Log.DiagnosticHandler discardDiagnosticHandler = new Log.DiscardDiagnosticHandler(log);
try {
Env<AttrContext> env = getEnvForPath(path);
Element res =
wrapInvocationOnResolveInstance(
FIND_IDENT_IN_TYPE, env, type, names.fromString(name), Kinds.KindSelector.VAR);
if (res.getKind().isField()) {
return (VariableElement) res;
} else if (res.getKind() == ElementKind.OTHER && ACCESSERROR.isInstance(res)) {
// Return the inaccessible field that was found
return (VariableElement) wrapInvocation(res, ACCESSERROR_ACCESS, null, null);
} else {
// Most likely didn't find the field and the Element is a SymbolNotFoundError
return null;
}
} finally {
log.popDiagnosticHandler(discardDiagnosticHandler);
}
}
/**
* Finds the local variable (including formal parameters) with name {@code name} in the given
* scope.
*
* @param name the name of the local variable
* @param path the tree path to the local scope
* @return the element for the local variable, {@code null} otherwise
*/
public @Nullable VariableElement findLocalVariableOrParameter(String name, TreePath path) {
Log.DiagnosticHandler discardDiagnosticHandler = new Log.DiscardDiagnosticHandler(log);
try {
Env<AttrContext> env = getEnvForPath(path);
Element res = wrapInvocationOnResolveInstance(FIND_VAR, env, names.fromString(name));
if (res.getKind() == ElementKind.LOCAL_VARIABLE || res.getKind() == ElementKind.PARAMETER) {
return (VariableElement) res;
} else {
// The Element might be FIELD or a SymbolNotFoundError.
return null;
}
} finally {
log.popDiagnosticHandler(discardDiagnosticHandler);
}
}
/**
* Finds the class literal with name {@code name}.
*
* <p>The method adheres to all the rules of Java's scoping (while also considering the imports)
* for name resolution.
*
* @param name the name of the class
* @param path the tree path to the local scope
* @return the element for the class
*/
public Element findClass(String name, TreePath path) {
Log.DiagnosticHandler discardDiagnosticHandler = new Log.DiscardDiagnosticHandler(log);
try {
Env<AttrContext> env = getEnvForPath(path);
return wrapInvocationOnResolveInstance(FIND_TYPE, env, names.fromString(name));
} finally {
log.popDiagnosticHandler(discardDiagnosticHandler);
}
}
/**
* Finds the class with name {@code name} in a given package.
*
* @param name the name of the class
* @param pck the PackageSymbol for the package
* @param path the tree path to the local scope
* @return the {@code ClassSymbol} for the class if it is found, {@code null} otherwise
*/
public @Nullable ClassSymbol findClassInPackage(String name, PackageSymbol pck, TreePath path) {
Log.DiagnosticHandler discardDiagnosticHandler = new Log.DiscardDiagnosticHandler(log);
try {
Env<AttrContext> env = getEnvForPath(path);
Element res =
wrapInvocationOnResolveInstance(
FIND_IDENT_IN_PACKAGE, env, pck, names.fromString(name), Kinds.KindSelector.TYP);
if (ElementUtils.isTypeElement(res)) {
return (ClassSymbol) res;
} else {
return null;
}
} finally {
log.popDiagnosticHandler(discardDiagnosticHandler);
}
}
/**
* Finds the method element for a given name and list of expected parameter types.
*
* <p>The method adheres to all the rules of Java's scoping (while also considering the imports)
* for name resolution.
*
* <p>(This method takes into account autoboxing.)
*
* @param methodName name of the method to find
* @param receiverType type of the receiver of the method
* @param path tree path
* @param argumentTypes types of arguments passed to the method call
* @return the method element (if found)
*/
public @Nullable ExecutableElement findMethod(
String methodName,
TypeMirror receiverType,
TreePath path,
java.util.List<TypeMirror> argumentTypes) {
Log.DiagnosticHandler discardDiagnosticHandler = new Log.DiscardDiagnosticHandler(log);
try {
Env<AttrContext> env = getEnvForPath(path);
Type site = (Type) receiverType;
Name name = names.fromString(methodName);
List<Type> argtypes = List.nil();
for (TypeMirror a : argumentTypes) {
argtypes = argtypes.append((Type) a);
}
List<Type> typeargtypes = List.nil();
boolean allowBoxing = true;
boolean useVarargs = false;
try {
// For some reason we have to set our own method context, which is rather ugly.
// TODO: find a nicer way to do this.
Object methodContext = buildMethodContext();
Object oldContext = getField(resolve, "currentResolutionContext");
setField(resolve, "currentResolutionContext", methodContext);
Element result =
wrapInvocationOnResolveInstance(
FIND_METHOD, env, site, name, argtypes, typeargtypes, allowBoxing, useVarargs);
setField(resolve, "currentResolutionContext", oldContext);
if (result.getKind() == ElementKind.METHOD || result.getKind() == ElementKind.CONSTRUCTOR) {
return (ExecutableElement) result;
}
return null;
} catch (Throwable t) {
Error err =
new AssertionError(
String.format(
"Unexpected reflection error in findMethod(%s, %s, ..., %s)",
methodName,
receiverType,
// path
argumentTypes));
err.initCause(t);
throw err;
}
} finally {
log.popDiagnosticHandler(discardDiagnosticHandler);
}
}
/** Build an instance of {@code Resolve$MethodResolutionContext}. */
protected Object buildMethodContext()
throws ClassNotFoundException, InstantiationException, IllegalAccessException,
InvocationTargetException, NoSuchFieldException {
// Class is not accessible, instantiate reflectively.
Class<?> methCtxClss =
Class.forName("com.sun.tools.javac.comp.Resolve$MethodResolutionContext");
Constructor<?> constructor = methCtxClss.getDeclaredConstructors()[0];
constructor.setAccessible(true);
Object methodContext = constructor.newInstance(resolve);
// we need to also initialize the fields attrMode and step
setField(methodContext, "attrMode", DeferredAttr.AttrMode.CHECK);
@SuppressWarnings("rawtypes")
List<?> phases = (List) getField(resolve, "methodResolutionSteps");
assert phases != null : "@AssumeAssertion(nullness): assumption";
setField(methodContext, "step", phases.get(1));
return methodContext;
}
/**
* Reflectively set a field.
*
* @param receiver the receiver in which to set the field
* @param fieldName name of field to set
* @param value new value for field
* @throws NoSuchFieldException if the field does not exist in the receiver
* @throws IllegalAccessException if the field is not accessible
*/
@SuppressWarnings({
"nullness:argument",
"interning:argument"
}) // assume that the fields all accept null and uninterned values
private void setField(Object receiver, String fieldName, @Nullable Object value)
throws NoSuchFieldException, IllegalAccessException {
Field f = receiver.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(receiver, value);
}
/** Reflectively get the value of a field. */
private @Nullable Object getField(Object receiver, String fieldName)
throws NoSuchFieldException, IllegalAccessException {
Field f = receiver.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
return f.get(receiver);
}
/**
* Wrap a method invocation on the {@code resolve} object.
*
* @param method the method to called
* @param args the arguments to the call
* @return the result of invoking the method on {@code resolve} (as the receiver) and the
* arguments
*/
private Symbol wrapInvocationOnResolveInstance(Method method, Object... args) {
return wrapInvocation(resolve, method, args);
}
/**
* Invoke a method reflectively.
*
* @param receiver the receiver
* @param method the method to called
* @param args the arguments to the call
* @return the result of invoking the method on the receiver and arguments
*/
private Symbol wrapInvocation(Object receiver, Method method, @Nullable Object... args) {
try {
@SuppressWarnings("nullness") // assume arguments are OK
@NonNull Symbol res = (Symbol) method.invoke(receiver, args);
return res;
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new BugInCF(
e,
"Unexpected reflection error in wrapInvocation(%s, %s, %s)",
receiver,
method,
Arrays.toString(args));
}
}
}