org.checkerframework.common.reflection.DefaultReflectionResolver Maven / Gradle / Ivy
Show all versions of checker Show documentation
package org.checkerframework.common.reflection;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.NewClassTree;
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.Flags;
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.Type;
import com.sun.tools.javac.code.TypeTag;
import com.sun.tools.javac.comp.AttrContext;
import com.sun.tools.javac.comp.Env;
import com.sun.tools.javac.comp.Resolve;
import com.sun.tools.javac.comp.Resolve.RecoveryLoadClass;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.tree.JCTree.JCMethodInvocation;
import com.sun.tools.javac.tree.JCTree.JCNewClass;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.Name;
import com.sun.tools.javac.util.Names;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.common.basetype.BaseTypeChecker;
import org.checkerframework.common.reflection.qual.Invoke;
import org.checkerframework.common.reflection.qual.MethodVal;
import org.checkerframework.common.reflection.qual.NewInstance;
import org.checkerframework.common.reflection.qual.UnknownMethod;
import org.checkerframework.framework.type.AnnotatedTypeFactory;
import org.checkerframework.framework.type.AnnotatedTypeFactory.ParameterizedExecutableType;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType;
import org.checkerframework.javacutil.AnnotationMirrorSet;
import org.checkerframework.javacutil.AnnotationProvider;
import org.checkerframework.javacutil.AnnotationUtils;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.TreeUtils;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
/**
* Default implementation of {@link ReflectionResolver}. It resolves calls to:
*
*
* - {@link Method#invoke(Object, Object...)}
*
- {@link Constructor#newInstance(Object...)}
*
*
* @checker_framework.manual #reflection-resolution Reflection resolution
*/
public class DefaultReflectionResolver implements ReflectionResolver {
/** Message prefix added to verbose reflection messages. */
public static final String MSG_PREFEX_REFLECTION = "[Reflection] ";
private final BaseTypeChecker checker;
private final AnnotationProvider provider;
private final ProcessingEnvironment processingEnv;
private final Trees trees;
private final boolean debug;
public DefaultReflectionResolver(
BaseTypeChecker checker,
MethodValAnnotatedTypeFactory methodValProvider,
boolean debug) {
this.checker = checker;
this.provider = methodValProvider;
this.processingEnv = checker.getProcessingEnvironment();
this.trees = Trees.instance(processingEnv);
this.debug = debug;
}
@Override
public boolean isReflectiveMethodInvocation(MethodInvocationTree tree) {
ExecutableElement methodElt = TreeUtils.elementFromUse(tree);
return (provider.getDeclAnnotation(methodElt, Invoke.class) != null
|| provider.getDeclAnnotation(methodElt, NewInstance.class) != null);
}
@Override
public ParameterizedExecutableType resolveReflectiveCall(
AnnotatedTypeFactory factory,
MethodInvocationTree tree,
ParameterizedExecutableType origResult) {
assert isReflectiveMethodInvocation(tree);
if (provider.getDeclAnnotation(TreeUtils.elementFromUse(tree), NewInstance.class) != null) {
return resolveConstructorCall(factory, tree, origResult);
} else {
return resolveMethodCall(factory, tree, origResult);
}
}
/**
* Resolves a call to {@link Method#invoke(Object, Object...)}.
*
* @param factory the {@link AnnotatedTypeFactory} of the underlying type system
* @param tree the method invocation tree that has to be resolved
* @param origResult the original result from {@code factory.methodFromUse}
* @return the resolved type of the call
*/
private ParameterizedExecutableType resolveMethodCall(
AnnotatedTypeFactory factory,
MethodInvocationTree tree,
ParameterizedExecutableType origResult) {
debugReflection("Try to resolve reflective method call: " + tree);
List possibleMethods = resolveReflectiveMethod(tree, factory);
// Reflective method could not be resolved
if (possibleMethods.isEmpty()) {
return origResult;
}
Set extends AnnotationMirror> returnLub = null;
Set extends AnnotationMirror> receiverGlb = null;
Set extends AnnotationMirror> paramsGlb = null;
// Iterate over all possible methods: lub return types, and glb receiver and parameter types
for (MethodInvocationTree resolvedTree : possibleMethods) {
debugReflection("Resolved method invocation: " + resolvedTree);
if (!checkMethodArguments(resolvedTree)) {
debugReflection(
"Spoofed tree's arguments did not match declaration" + resolvedTree);
// Calling methodFromUse on these sorts of trees will cause an assertion to fail in
// QualifierPolymorphism.PolyCollector.visitArray(...)
continue;
}
ParameterizedExecutableType resolvedResult = factory.methodFromUse(resolvedTree);
AnnotatedTypeMirror returnType = resolvedResult.executableType.getReturnType();
TypeMirror returnTM = returnType.getUnderlyingType();
// Lub return types
returnLub = lub(returnLub, returnTM, returnType.getAnnotations(), returnTM, factory);
// Glb receiver types (actual method receiver is passed as first
// argument to invoke(Object, Object[]))
// Check for static methods whose receiver is null
AnnotatedTypeMirror receiverType = resolvedResult.executableType.getReceiverType();
if (receiverType == null) {
// If the method is static the first argument to Method.invoke isn't used, so assume
// top.
if (receiverGlb == null) {
receiverGlb =
new AnnotationMirrorSet(
factory.getQualifierHierarchy().getTopAnnotations());
}
} else {
TypeMirror receiverTM = receiverType.getUnderlyingType();
receiverGlb =
glb(
receiverGlb,
receiverTM,
receiverType.getAnnotations(),
receiverTM,
factory);
}
// Glb parameter types. All formal parameter types get combined together because
// Method#invoke takes as argument an array of parameter types, so there is no way to
// distinguish the types of different formal parameters.
for (AnnotatedTypeMirror mirror : resolvedResult.executableType.getParameterTypes()) {
TypeMirror mirrorTM = mirror.getUnderlyingType();
paramsGlb = glb(paramsGlb, mirrorTM, mirror.getAnnotations(), mirrorTM, factory);
}
}
if (returnLub == null) {
// None of the spoofed tree's arguments matched the declared method
return origResult;
}
/*
* Clear all original (return, receiver, parameter type) annotations and
* set lub/glb annotations from resolved method(s)
*/
// return value
origResult.executableType.getReturnType().clearAnnotations();
origResult.executableType.getReturnType().addAnnotations(returnLub);
// receiver type
origResult.executableType.getParameterTypes().get(0).clearAnnotations();
origResult.executableType.getParameterTypes().get(0).addAnnotations(receiverGlb);
// parameter types
if (paramsGlb != null) {
AnnotatedArrayType origArrayType =
(AnnotatedArrayType) origResult.executableType.getParameterTypes().get(1);
origArrayType.getComponentType().clearAnnotations();
origArrayType.getComponentType().addAnnotations(paramsGlb);
}
debugReflection("Resolved annotations: " + origResult.executableType);
return origResult;
}
/**
* Checks that arguments of a method invocation are consistent with their corresponding
* parameters.
*
* @param resolvedTree a method invocation
* @return true if arguments are consistent with parameters
*/
private boolean checkMethodArguments(MethodInvocationTree resolvedTree) {
// type.getKind() == actualType.getKind()
ExecutableElement methodDecl = TreeUtils.elementFromUse(resolvedTree);
return checkArguments(methodDecl.getParameters(), resolvedTree.getArguments());
}
/**
* Checks that arguments of a constructor invocation are consistent with their corresponding
* parameters.
*
* @param resolvedTree a constructor invocation
* @return true if arguments are consistent with parameters
*/
private boolean checkNewClassArguments(NewClassTree resolvedTree) {
ExecutableElement methodDecl = TreeUtils.elementFromUse(resolvedTree);
return checkArguments(methodDecl.getParameters(), resolvedTree.getArguments());
}
/**
* Checks that argument are consistent with their corresponding parameter types. Common code
* used by {@link #checkMethodArguments} and {@link #checkNewClassArguments}.
*
* @param parameters formal parameters
* @param arguments actual arguments
* @return true if argument are consistent with their corresponding parameter types
*/
private boolean checkArguments(
List extends VariableElement> parameters, List extends ExpressionTree> arguments) {
if (parameters.size() != arguments.size()) {
return false;
}
for (int i = 0; i < parameters.size(); i++) {
VariableElement param = parameters.get(i);
ExpressionTree arg = arguments.get(i);
TypeMirror argType = TreeUtils.typeOf(arg);
TypeMirror paramType = param.asType();
if (argType.getKind() == TypeKind.ARRAY && paramType.getKind() != argType.getKind()) {
return false;
}
}
return true;
}
/**
* Resolves a call to {@link Constructor#newInstance(Object...)}.
*
* @param factory the {@link AnnotatedTypeFactory} of the underlying type system
* @param tree the method invocation tree (representing a constructor call) that has to be
* resolved
* @param origResult the original result from {@code factory.methodFromUse}
* @return the resolved type of the call
*/
private ParameterizedExecutableType resolveConstructorCall(
AnnotatedTypeFactory factory,
MethodInvocationTree tree,
ParameterizedExecutableType origResult) {
debugReflection("Try to resolve reflective constructor call: " + tree);
List possibleConstructors = resolveReflectiveConstructor(tree, factory);
// Reflective constructor could not be resolved
if (possibleConstructors.isEmpty()) {
return origResult;
}
Set extends AnnotationMirror> returnLub = null;
Set extends AnnotationMirror> paramsGlb = null;
// Iterate over all possible constructors: lub return types and glb parameter types
for (JCNewClass resolvedTree : possibleConstructors) {
debugReflection("Resolved constructor invocation: " + resolvedTree);
if (!checkNewClassArguments(resolvedTree)) {
debugReflection(
"Spoofed tree's arguments did not match declaration" + resolvedTree);
// Calling methodFromUse on these sorts of trees will cause an assertion to fail in
// QualifierPolymorphism.PolyCollector.visitArray(...)
continue;
}
ParameterizedExecutableType resolvedResult = factory.constructorFromUse(resolvedTree);
AnnotatedExecutableType executableType = resolvedResult.executableType;
AnnotatedTypeMirror returnType = executableType.getReturnType();
TypeMirror returnTM = returnType.getUnderlyingType();
// Lub return types
returnLub = lub(returnLub, returnTM, returnType.getAnnotations(), returnTM, factory);
// Glb parameter types
for (AnnotatedTypeMirror mirror : executableType.getParameterTypes()) {
TypeMirror mirrorTM = mirror.getUnderlyingType();
paramsGlb = glb(paramsGlb, mirrorTM, mirror.getAnnotations(), mirrorTM, factory);
}
}
if (returnLub == null) {
// None of the spoofed tree's arguments matched the declared method
return origResult;
}
/*
* Clear all original (return, parameter type) annotations and set
* lub/glb annotations from resolved constructors.
*/
// return value
origResult.executableType.getReturnType().clearAnnotations();
origResult.executableType.getReturnType().addAnnotations(returnLub);
// parameter types
if (paramsGlb != null) {
AnnotatedArrayType origArrayType =
(AnnotatedArrayType) origResult.executableType.getParameterTypes().get(0);
origArrayType.getComponentType().clearAnnotations();
origArrayType.getComponentType().addAnnotations(paramsGlb);
}
debugReflection("Resolved annotations: " + origResult.executableType);
return origResult;
}
/**
* Resolves a reflective method call and returns all possible corresponding method calls.
*
* @param tree the MethodInvocationTree AST node that is to be resolved (Method.invoke)
* @return a (potentially empty) list of all resolved MethodInvocationTrees
*/
private List resolveReflectiveMethod(
MethodInvocationTree tree, AnnotatedTypeFactory reflectionFactory) {
assert isReflectiveMethodInvocation(tree);
JCMethodInvocation methodInvocation = (JCMethodInvocation) tree;
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
TreeMaker make = TreeMaker.instance(context);
TreePath path = reflectionFactory.getPath(tree);
JavacScope scope = (JavacScope) trees.getScope(path);
Env env = scope.getEnv();
boolean unknown = isUnknownMethod(tree);
AnnotationMirror estimate = getMethodVal(tree);
if (estimate == null) {
debugReflection("MethodVal is unknown for: " + tree);
debugReflection("UnknownMethod annotation: " + unknown);
return Collections.emptyList();
}
debugReflection("MethodVal type system annotations: " + estimate);
List listClassNames =
AnnotationUtils.getElementValueArray(
estimate, reflectionFactory.methodValClassNameElement, String.class);
List listMethodNames =
AnnotationUtils.getElementValueArray(
estimate, reflectionFactory.methodValMethodNameElement, String.class);
List listParamLengths =
AnnotationUtils.getElementValueArray(
estimate, reflectionFactory.methodValParamsElement, Integer.class);
assert listClassNames.size() == listMethodNames.size()
&& listClassNames.size() == listParamLengths.size();
List methodInvocations = new ArrayList<>();
for (int i = 0; i < listClassNames.size(); ++i) {
String className = listClassNames.get(i);
String methodName = listMethodNames.get(i);
int paramLength = listParamLengths.get(i);
// Get receiver, which is always the first argument of the invoke method
JCExpression receiver = methodInvocation.args.head;
// The remaining list contains the arguments
com.sun.tools.javac.util.List args = methodInvocation.args.tail;
// Resolve the Symbol(s) for the current method
for (Symbol symbol : getMethodSymbolsfor(className, methodName, paramLength, env)) {
if (!processingEnv.getTypeUtils().isSubtype(receiver.type, symbol.owner.type)) {
continue;
}
if ((symbol.flags() & Flags.PUBLIC) > 0) {
debugReflection("Resolved public method: " + symbol.owner + "." + symbol);
} else {
debugReflection("Resolved non-public method: " + symbol.owner + "." + symbol);
}
JCExpression method = TreeUtils.Select(make, receiver, symbol);
args = getCorrectedArgs(symbol, args);
// Build method invocation tree depending on the number of
// parameters
JCMethodInvocation syntTree =
paramLength > 0 ? make.App(method, args) : make.App(method);
// add method invocation tree to the list of possible method invocations
methodInvocations.add(syntTree);
}
}
return methodInvocations;
}
private com.sun.tools.javac.util.List getCorrectedArgs(
Symbol symbol, com.sun.tools.javac.util.List args) {
if (symbol.getKind() == ElementKind.METHOD) {
MethodSymbol method = ((MethodSymbol) symbol);
// neg means too many arg,
// pos means to few args
int diff = method.getParameters().size() - args.size();
if (diff > 0) {
// means too few args
int origArgSize = args.size();
for (int i = 0; i < diff; i++) {
args = args.append(args.get(i % origArgSize));
}
} else if (diff < 0) {
// means too many args
com.sun.tools.javac.util.List tmp =
com.sun.tools.javac.util.List.nil();
for (int i = 0; i < method.getParameters().size(); i++) {
tmp = tmp.append(args.get(i));
}
args = tmp;
}
}
return args;
}
/**
* Resolves a reflective constructor call and returns all possible corresponding constructor
* calls.
*
* @param tree the MethodInvocationTree AST node that is to be resolved
* (Constructor.newInstance)
* @return a (potentially empty) list of all resolved MethodInvocationTrees
*/
private List resolveReflectiveConstructor(
MethodInvocationTree tree, AnnotatedTypeFactory reflectionFactory) {
assert isReflectiveMethodInvocation(tree);
JCMethodInvocation methodInvocation = (JCMethodInvocation) tree;
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
TreeMaker make = TreeMaker.instance(context);
TreePath path = reflectionFactory.getPath(tree);
JavacScope scope = (JavacScope) trees.getScope(path);
Env env = scope.getEnv();
AnnotationMirror estimate = getMethodVal(tree);
if (estimate == null) {
debugReflection("MethodVal is unknown for: " + tree);
debugReflection("UnknownMethod annotation: " + isUnknownMethod(tree));
return Collections.emptyList();
}
debugReflection("MethodVal type system annotations: " + estimate);
List listClassNames =
AnnotationUtils.getElementValueArray(
estimate, reflectionFactory.methodValClassNameElement, String.class);
List listParamLengths =
AnnotationUtils.getElementValueArray(
estimate, reflectionFactory.methodValParamsElement, Integer.class);
assert listClassNames.size() == listParamLengths.size();
List constructorInvocations = new ArrayList<>();
for (int i = 0; i < listClassNames.size(); ++i) {
String className = listClassNames.get(i);
int paramLength = listParamLengths.get(i);
// Resolve the Symbol for the current constructor
for (Symbol symbol : getConstructorSymbolsfor(className, paramLength, env)) {
debugReflection("Resolved constructor: " + symbol.owner + "." + symbol);
JCNewClass syntTree = (JCNewClass) make.Create(symbol, methodInvocation.args);
// add constructor invocation tree to the list of possible constructor invocations
constructorInvocations.add(syntTree);
}
}
return constructorInvocations;
}
private AnnotationMirror getMethodVal(MethodInvocationTree tree) {
return provider.getAnnotationMirror(TreeUtils.getReceiverTree(tree), MethodVal.class);
}
/** Returns true if the receiver's type is @UnknownMethod. */
private boolean isUnknownMethod(MethodInvocationTree tree) {
return provider.getAnnotationMirror(TreeUtils.getReceiverTree(tree), UnknownMethod.class)
!= null;
}
/**
* Get set of MethodSymbols based on class name, method name, and parameter length.
*
* @param className the class that contains the method
* @param methodName the method's name
* @param paramLength the number of parameters
* @param env the environment
* @return the (potentially empty) set of corresponding method Symbol(s)
*/
private List getMethodSymbolsfor(
String className, String methodName, int paramLength, Env env) {
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
Resolve resolve = Resolve.instance(context);
Names names = Names.instance(context);
Symbol sym = getSymbol(className, env, names, resolve);
if (!sym.exists()) {
debugReflection("Unable to resolve class: " + className);
return Collections.emptyList();
}
// The common case is probably that `result` is a singleton at method exit.
List result = new ArrayList<>();
ClassSymbol classSym = (ClassSymbol) sym;
while (classSym != null) {
for (Symbol s : getEnclosedElements(classSym)) {
// check all member methods
if (s.getKind() == ElementKind.METHOD) {
// Check for method name and number of arguments
if (names.fromString(methodName) == s.name
&& ((MethodSymbol) s).getParameters().size() == paramLength) {
result.add(s);
}
}
}
if (!result.isEmpty()) {
break;
}
Type t = classSym.getSuperclass();
if (!t.hasTag(TypeTag.CLASS) || t.isErroneous()) {
break;
}
classSym = (ClassSymbol) t.tsym;
}
if (result.isEmpty()) {
debugReflection("Unable to resolve method: " + className + "@" + methodName);
}
return result;
}
/**
* Get set of Symbols for constructors based on class name and parameter length.
*
* @return the (potentially empty) set of corresponding constructor Symbol(s)
*/
private List getConstructorSymbolsfor(
String className, int paramLength, Env env) {
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
Resolve resolve = Resolve.instance(context);
Names names = Names.instance(context);
Symbol symClass = getSymbol(className, env, names, resolve);
if (!symClass.exists()) {
debugReflection("Unable to resolve class: " + className);
return Collections.emptyList();
}
// TODO: Should this be used instead of the below??
ElementFilter.constructorsIn(getEnclosedElements(symClass));
// The common case is probably that there is one constructor of the given parameter length.
List result = new ArrayList<>(2);
for (Symbol s : getEnclosedElements(symClass)) {
// Check all constructors
if (s.getKind() == ElementKind.CONSTRUCTOR) {
// Check for number of parameters
if (((MethodSymbol) s).getParameters().size() == paramLength) {
result.add(s);
}
}
}
if (result.isEmpty()) {
debugReflection("Unable to resolve constructor!");
}
return result;
}
private Symbol getSymbol(String className, Env env, Names names, Resolve resolve) {
Method loadClass;
try {
loadClass =
Resolve.class.getDeclaredMethod(
"loadClass", Env.class, Name.class, RecoveryLoadClass.class);
loadClass.setAccessible(true);
} catch (SecurityException | NoSuchMethodException | IllegalArgumentException e) {
// A problem with javac is serious and must be reported.
throw new BugInCF("Error in obtaining reflective method.", e);
}
try {
RecoveryLoadClass noRecovery = (e, n) -> null;
return (Symbol) loadClass.invoke(resolve, env, names.fromString(className), noRecovery);
} catch (SecurityException
| IllegalAccessException
| IllegalArgumentException
| InvocationTargetException e) {
// A problem with javac is serious and must be reported.
throw new BugInCF("Error in invoking reflective method.", e);
}
}
/**
* Determine the enclosed elements for an element. This wrapper is useful to avoid a signature
* change in the called method.
*
* @param sym the element
* @return the enclosed elements
*/
@SuppressWarnings("ASTHelpersSuggestions") // Use local helper.
private static List getEnclosedElements(Symbol sym) {
return sym.getEnclosedElements();
}
/**
* Build lub of the two types (represented by sets {@code set1} and {@code set2}) using the
* provided AnnotatedTypeFactory.
*
* If {@code set1} is {@code null} or empty, {@code set2} is returned.
*
* @param set1 the first type
* @param tm1 the type that is annotated by qualifier1
* @param set2 the second type
* @param tm2 the type that is annotated by qualifier2
* @param atypeFactory the type factory
* @return the lub of the two types
*/
private Set extends AnnotationMirror> lub(
@Nullable Set extends AnnotationMirror> set1,
TypeMirror tm1,
Set extends AnnotationMirror> set2,
TypeMirror tm2,
AnnotatedTypeFactory atypeFactory) {
if (set1 == null || set1.isEmpty()) {
return set2;
} else {
return atypeFactory
.getQualifierHierarchy()
.leastUpperBoundsShallow(set1, tm1, set2, tm2);
}
}
/**
* Build glb of the two types (represented by sets {@code set1} and {@code set2}) using the
* provided AnnotatedTypeFactory.
*
*
If {@code set1} is {@code null} or empty, {@code set2} is returned.
*
* @param set1 the first type
* @param tm1 the type that is annotated by qualifier1
* @param set2 the second type
* @param tm2 the type that is annotated by qualifier2
* @param atypeFactory the type factory
* @return the glb of the two types
*/
private Set extends AnnotationMirror> glb(
@Nullable Set extends AnnotationMirror> set1,
TypeMirror tm1,
Set extends AnnotationMirror> set2,
TypeMirror tm2,
AnnotatedTypeFactory atypeFactory) {
if (set1 == null || set1.isEmpty()) {
return set2;
} else {
return atypeFactory
.getQualifierHierarchy()
.greatestLowerBoundsShallow(set1, tm1, set2, tm2);
}
}
/**
* Reports debug information about the reflection resolution iff the corresponding debug flag is
* set.
*
* @param msg the debug message
*/
private void debugReflection(String msg) {
if (debug) {
checker.message(javax.tools.Diagnostic.Kind.NOTE, MSG_PREFEX_REFLECTION + msg);
}
}
}