blob: 95fdcd627387f0914354c2a8e9a85bbfc09bd67e [file] [log] [blame]
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 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;
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.javacutil.AnnotationProvider;
import org.checkerframework.javacutil.AnnotationUtils;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.TreeUtils;
/**
* Default implementation of {@link ReflectionResolver}. It resolves calls to:
*
* <ul>
* <li>{@link Method#invoke(Object, Object...)}
* <li>{@link Constructor#newInstance(Object...)}
* </ul>
*
* @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) {
return ((provider.getDeclAnnotation(TreeUtils.elementFromTree(tree), Invoke.class) != null
|| provider.getDeclAnnotation(TreeUtils.elementFromTree(tree), NewInstance.class) != null));
}
@Override
public ParameterizedExecutableType resolveReflectiveCall(
AnnotatedTypeFactory factory,
MethodInvocationTree tree,
ParameterizedExecutableType origResult) {
assert isReflectiveMethodInvocation(tree);
if (provider.getDeclAnnotation(TreeUtils.elementFromTree(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<MethodInvocationTree> 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);
// Lub return types
returnLub =
lub(returnLub, resolvedResult.executableType.getReturnType().getAnnotations(), factory);
// Glb receiver types (actual method receiver is passed as first
// argument to invoke(Object, Object[]))
// Check for static methods whose receiver is null
if (resolvedResult.executableType.getReceiverType() == null) {
// If the method is static the first argument to Method.invoke isn't used, so assume top.
receiverGlb =
glb(receiverGlb, factory.getQualifierHierarchy().getTopAnnotations(), factory);
} else {
receiverGlb =
glb(
receiverGlb,
resolvedResult.executableType.getReceiverType().getAnnotations(),
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()) {
paramsGlb = glb(paramsGlb, mirror.getAnnotations(), 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<JCNewClass> 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);
// Lub return types
returnLub =
lub(returnLub, resolvedResult.executableType.getReturnType().getAnnotations(), factory);
// Glb parameter types
for (AnnotatedTypeMirror mirror : resolvedResult.executableType.getParameterTypes()) {
paramsGlb = glb(paramsGlb, mirror.getAnnotations(), 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 node that is to be resolved (Method.invoke)
* @return a (potentially empty) list of all resolved MethodInvocationTrees
*/
private List<MethodInvocationTree> 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<AttrContext> 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<String> listClassNames =
AnnotationUtils.getElementValueArray(
estimate, reflectionFactory.methodValClassNameElement, String.class);
List<String> listMethodNames =
AnnotationUtils.getElementValueArray(
estimate, reflectionFactory.methodValMethodNameElement, String.class);
List<Integer> listParamLengths =
AnnotationUtils.getElementValueArray(
estimate, reflectionFactory.methodValParamsElement, Integer.class);
assert listClassNames.size() == listMethodNames.size()
&& listClassNames.size() == listParamLengths.size();
List<MethodInvocationTree> methods = 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<JCExpression> 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 = make.Select(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 methods
methods.add(syntTree);
}
}
return methods;
}
private com.sun.tools.javac.util.List<JCExpression> getCorrectedArgs(
Symbol symbol, com.sun.tools.javac.util.List<JCExpression> 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<JCExpression> 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 node that is to be resolved (Constructor.newInstance)
* @return a (potentially empty) list of all resolved MethodInvocationTrees
*/
private List<JCNewClass> 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<AttrContext> 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<String> listClassNames =
AnnotationUtils.getElementValueArray(
estimate, reflectionFactory.methodValClassNameElement, String.class);
List<Integer> listParamLengths =
AnnotationUtils.getElementValueArray(
estimate, reflectionFactory.methodValParamsElement, Integer.class);
assert listClassNames.size() == listParamLengths.size();
List<JCNewClass> constructors = 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
// constructors
constructors.add(syntTree);
}
}
return constructors;
}
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<Symbol> getMethodSymbolsfor(
String className, String methodName, int paramLength, Env<AttrContext> 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<Symbol> result = new ArrayList<>();
ClassSymbol classSym = (ClassSymbol) sym;
while (classSym != null) {
for (Symbol s : classSym.getEnclosedElements()) {
// 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<Symbol> getConstructorSymbolsfor(
String className, int paramLength, Env<AttrContext> 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(symClass.getEnclosedElements());
// The common case is probably that `result` is a singleton at method exit.
List<Symbol> result = new ArrayList<>();
for (Symbol s : symClass.getEnclosedElements()) {
// 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<AttrContext> 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);
}
}
/**
* Build lub of the two types (represented by sets {@code set1} and {@code set2}) using the
* provided AnnotatedTypeFactory.
*
* <p>If {@code set1} is {@code null} or empty, {@code set2} is returned.
*/
private Set<? extends AnnotationMirror> lub(
Set<? extends AnnotationMirror> set1,
Set<? extends AnnotationMirror> set2,
AnnotatedTypeFactory factory) {
if (set1 == null || set1.isEmpty()) {
return set2;
} else {
return factory.getQualifierHierarchy().leastUpperBounds(set1, set2);
}
}
/**
* Build glb of the two types (represented by sets {@code set1} and {@code set2}) using the
* provided AnnotatedTypeFactory.
*
* <p>If {@code set1} is {@code null} or empty, {@code set2} is returned.
*/
private Set<? extends AnnotationMirror> glb(
Set<? extends AnnotationMirror> set1,
Set<? extends AnnotationMirror> set2,
AnnotatedTypeFactory factory) {
if (set1 == null || set1.isEmpty()) {
return set2;
} else {
return factory.getQualifierHierarchy().greatestLowerBounds(set1, set2);
}
}
/**
* 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);
}
}
}