| package org.checkerframework.common.value; |
| |
| import com.sun.source.tree.ExpressionTree; |
| import com.sun.source.tree.MethodInvocationTree; |
| import com.sun.source.tree.NewClassTree; |
| import java.lang.reflect.Constructor; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.InvocationTargetException; |
| import java.lang.reflect.Method; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| import javax.lang.model.element.Element; |
| import javax.lang.model.element.ExecutableElement; |
| import javax.lang.model.type.DeclaredType; |
| import javax.lang.model.type.TypeMirror; |
| import org.checkerframework.checker.signature.qual.CanonicalNameOrEmpty; |
| import org.checkerframework.checker.signature.qual.ClassGetName; |
| import org.checkerframework.common.basetype.BaseTypeChecker; |
| import org.checkerframework.javacutil.ElementUtils; |
| import org.checkerframework.javacutil.TreeUtils; |
| import org.checkerframework.javacutil.TypesUtils; |
| import org.plumelib.util.CollectionsPlume; |
| import org.plumelib.util.StringsPlume; |
| |
| /** |
| * Evaluates expressions (such as method calls and field accesses) at compile time, to determine |
| * whether they have compile-time constant values. |
| */ |
| public class ReflectiveEvaluator { |
| |
| /** The checker that is using this ReflectiveEvaluator. */ |
| private BaseTypeChecker checker; |
| |
| /** |
| * Whether to report warnings about problems with evaluation. Controlled by the -AreportEvalWarns |
| * command-line option. |
| */ |
| private boolean reportWarnings; |
| |
| public ReflectiveEvaluator( |
| BaseTypeChecker checker, ValueAnnotatedTypeFactory factory, boolean reportWarnings) { |
| this.checker = checker; |
| this.reportWarnings = reportWarnings; |
| } |
| |
| /** |
| * Returns all possible values that the method may return, or null if the method could not be |
| * evaluated. |
| * |
| * @param allArgValues a list of list where the first list corresponds to all possible values for |
| * the first argument. Pass null to indicate that the method has no arguments. |
| * @param receiverValues a list of possible receiver values. null indicates that the method has no |
| * receiver. |
| * @param tree location to report any errors |
| * @return all possible values that the method may return, or null if the method could not be |
| * evaluated |
| */ |
| public List<?> evaluateMethodCall( |
| List<List<?>> allArgValues, List<?> receiverValues, MethodInvocationTree tree) { |
| Method method = getMethodObject(tree); |
| if (method == null) { |
| return null; |
| } |
| |
| if (receiverValues == null) { |
| // Method does not have a receiver |
| // the first parameter of Method.invoke should be null |
| receiverValues = Collections.singletonList(null); |
| } |
| |
| List<Object[]> listOfArguments; |
| if (allArgValues == null) { |
| // Method does not have arguments |
| listOfArguments = Collections.singletonList(null); |
| } else { |
| // Find all possible argument sets |
| listOfArguments = cartesianProduct(allArgValues, allArgValues.size() - 1); |
| } |
| |
| if (method.isVarArgs()) { |
| int numberOfParameters = method.getParameterTypes().length; |
| listOfArguments = |
| CollectionsPlume.mapList( |
| (Object[] args) -> normalizeVararg(args, numberOfParameters), listOfArguments); |
| } |
| |
| List<Object> results = new ArrayList<>(listOfArguments.size()); |
| for (Object[] arguments : listOfArguments) { |
| for (Object receiver : receiverValues) { |
| try { |
| results.add(method.invoke(receiver, arguments)); |
| } catch (InvocationTargetException e) { |
| if (reportWarnings) { |
| checker.reportWarning( |
| tree, "method.evaluation.exception", method, e.getTargetException().toString()); |
| } |
| // Method evaluation will always fail, so don't bother |
| // trying again |
| return null; |
| } catch (ExceptionInInitializerError e) { |
| if (reportWarnings) { |
| checker.reportWarning( |
| tree, "method.evaluation.exception", method, e.getCause().toString()); |
| } |
| return null; |
| } catch (IllegalArgumentException e) { |
| if (reportWarnings) { |
| String args = StringsPlume.join(", ", arguments); |
| checker.reportWarning( |
| tree, "method.evaluation.exception", method, e.getLocalizedMessage() + ": " + args); |
| } |
| return null; |
| } catch (Throwable e) { |
| // Catch any exception thrown because they shouldn't crash the type checker. |
| if (reportWarnings) { |
| checker.reportWarning(tree, "method.evaluation.failed", method); |
| } |
| return null; |
| } |
| } |
| } |
| return results; |
| } |
| |
| /** |
| * This method normalizes an array of arguments to a varargs method by changing the arguments |
| * associated with the varargs parameter into an array. |
| * |
| * @param arguments an array of arguments for {@code method}. The length is at least {@code |
| * numberOfParameters - 1}. |
| * @param numberOfParameters number of parameters of the vararg method |
| * @return the length of the array is exactly {@code numberOfParameters} |
| */ |
| private Object[] normalizeVararg(Object[] arguments, int numberOfParameters) { |
| |
| if (arguments == null) { |
| // null means no arguments. For varargs no arguments is an empty array. |
| arguments = new Object[] {}; |
| } |
| Object[] newArgs = new Object[numberOfParameters]; |
| Object[] varArgsArray; |
| int numOfVarArgs = arguments.length - numberOfParameters + 1; |
| if (numOfVarArgs > 0) { |
| System.arraycopy(arguments, 0, newArgs, 0, numberOfParameters - 1); |
| varArgsArray = new Object[numOfVarArgs]; |
| System.arraycopy(arguments, numberOfParameters - 1, varArgsArray, 0, numOfVarArgs); |
| } else { |
| System.arraycopy(arguments, 0, newArgs, 0, numberOfParameters - 1); |
| varArgsArray = new Object[] {}; |
| } |
| newArgs[numberOfParameters - 1] = varArgsArray; |
| return newArgs; |
| } |
| |
| /** |
| * Method for reflectively obtaining a method object so it can (potentially) be statically |
| * executed by the checker for constant propagation. |
| * |
| * @param tree a method invocation tree |
| * @return the Method object corresponding to the method invocation tree |
| */ |
| private Method getMethodObject(MethodInvocationTree tree) { |
| final ExecutableElement ele = TreeUtils.elementFromUse(tree); |
| List<Class<?>> paramClasses = null; |
| try { |
| @CanonicalNameOrEmpty String className = |
| TypesUtils.getQualifiedName((DeclaredType) ele.getEnclosingElement().asType()); |
| paramClasses = getParameterClasses(ele); |
| @SuppressWarnings("signature") // https://tinyurl.com/cfissue/658 for Class.toString |
| Class<?> clazz = Class.forName(className.toString()); |
| Method method = |
| clazz.getMethod(ele.getSimpleName().toString(), paramClasses.toArray(new Class<?>[0])); |
| @SuppressWarnings("deprecation") // TODO: find alternative |
| boolean acc = method.isAccessible(); |
| if (!acc) { |
| method.setAccessible(true); |
| } |
| return method; |
| } catch (ClassNotFoundException | UnsupportedClassVersionError | NoClassDefFoundError e) { |
| if (reportWarnings) { |
| checker.reportWarning(tree, "class.find.failed", ele.getEnclosingElement()); |
| } |
| return null; |
| |
| } catch (Throwable e) { |
| // The class we attempted to getMethod from inside the |
| // call to getMethodObject. |
| Element classElem = ele.getEnclosingElement(); |
| |
| if (classElem == null) { |
| if (reportWarnings) { |
| checker.reportWarning(tree, "method.find.failed", ele.getSimpleName(), paramClasses); |
| } |
| } else { |
| if (reportWarnings) { |
| checker.reportWarning( |
| tree, "method.find.failed.in.class", ele.getSimpleName(), paramClasses, classElem); |
| } |
| } |
| return null; |
| } |
| } |
| |
| /** |
| * Returns the classes of the given method's formal parameters. |
| * |
| * @param ele a method or constructor |
| * @return the classes of the given method's formal parameters |
| * @throws ClassNotFoundException if the class cannot be found |
| */ |
| private List<Class<?>> getParameterClasses(ExecutableElement ele) throws ClassNotFoundException { |
| return CollectionsPlume.mapList( |
| (Element e) -> TypesUtils.getClassFromType(ElementUtils.getType(e)), ele.getParameters()); |
| } |
| |
| private List<Object[]> cartesianProduct(List<List<?>> allArgValues, int whichArg) { |
| List<?> argValues = allArgValues.get(whichArg); |
| List<Object[]> tuples = new ArrayList<>(); |
| |
| for (Object value : argValues) { |
| if (whichArg == 0) { |
| Object[] objects = new Object[allArgValues.size()]; |
| objects[0] = value; |
| tuples.add(objects); |
| } else { |
| List<Object[]> lastTuples = cartesianProduct(allArgValues, whichArg - 1); |
| List<Object[]> copies = copy(lastTuples); |
| for (Object[] copy : copies) { |
| copy[whichArg] = value; |
| } |
| tuples.addAll(copies); |
| } |
| } |
| return tuples; |
| } |
| |
| /** |
| * Returns a depth-2 copy of the given list. In the returned value, the list and the arrays in it |
| * are new, but the elements of the arrays are shared with the argument. |
| * |
| * @param lastTuples a list of arrays |
| * @return a depth-2 copy of the given list |
| */ |
| private List<Object[]> copy(List<Object[]> lastTuples) { |
| return CollectionsPlume.mapList( |
| (Object[] list) -> Arrays.copyOf(list, list.length), lastTuples); |
| } |
| |
| /** |
| * Return the value of a static field access. Return null if accessing the field reflectively |
| * fails. |
| * |
| * @param classname the class containing the field |
| * @param fieldName the name of the field |
| * @param tree the static field access in the program; a MemberSelectTree or an IdentifierTree; |
| * used for diagnostics |
| * @return the value of the static field access, or null if it cannot be determined |
| */ |
| public Object evaluateStaticFieldAccess( |
| @ClassGetName String classname, String fieldName, ExpressionTree tree) { |
| try { |
| Class<?> recClass = Class.forName(classname); |
| Field field = recClass.getField(fieldName); |
| return field.get(recClass); |
| |
| } catch (ClassNotFoundException | UnsupportedClassVersionError | NoClassDefFoundError e) { |
| if (reportWarnings) { |
| checker.reportWarning( |
| tree, "class.find.failed", classname, e.getClass() + ": " + e.getMessage()); |
| } |
| return null; |
| } catch (Throwable e) { |
| // Catch all exception so that the checker doesn't crash |
| if (reportWarnings) { |
| checker.reportWarning( |
| tree, |
| "field.access.failed", |
| fieldName, |
| classname, |
| e.getClass() + ": " + e.getMessage()); |
| } |
| return null; |
| } |
| } |
| |
| public List<?> evaluteConstructorCall( |
| ArrayList<List<?>> argValues, NewClassTree tree, TypeMirror typeToCreate) { |
| Constructor<?> constructor; |
| try { |
| // get the constructor |
| constructor = getConstructorObject(tree, typeToCreate); |
| } catch (Throwable e) { |
| // Catch all exception so that the checker doesn't crash |
| if (reportWarnings) { |
| checker.reportWarning(tree, "constructor.invocation.failed"); |
| } |
| return null; |
| } |
| if (constructor == null) { |
| return null; |
| } |
| |
| List<Object[]> listOfArguments; |
| if (argValues == null) { |
| // Method does not have arguments |
| listOfArguments = Collections.singletonList(null); |
| } else { |
| // Find all possible argument sets |
| listOfArguments = cartesianProduct(argValues, argValues.size() - 1); |
| } |
| |
| List<Object> results = new ArrayList<>(listOfArguments.size()); |
| for (Object[] arguments : listOfArguments) { |
| try { |
| results.add(constructor.newInstance(arguments)); |
| } catch (Throwable e) { |
| if (reportWarnings) { |
| checker.reportWarning( |
| tree, |
| "constructor.evaluation.failed", |
| typeToCreate, |
| StringsPlume.join(", ", arguments)); |
| } |
| return null; |
| } |
| } |
| return results; |
| } |
| |
| private Constructor<?> getConstructorObject(NewClassTree tree, TypeMirror typeToCreate) |
| throws ClassNotFoundException, NoSuchMethodException { |
| ExecutableElement ele = TreeUtils.elementFromUse(tree); |
| List<Class<?>> paramClasses = getParameterClasses(ele); |
| Class<?> recClass = boxPrimitives(TypesUtils.getClassFromType(typeToCreate)); |
| Constructor<?> constructor = recClass.getConstructor(paramClasses.toArray(new Class<?>[0])); |
| return constructor; |
| } |
| /** |
| * Returns the box primitive type if the passed type is an (unboxed) primitive. Otherwise it |
| * returns the passed type |
| */ |
| private static Class<?> boxPrimitives(Class<?> type) { |
| if (type == byte.class) { |
| return Byte.class; |
| } else if (type == short.class) { |
| return Short.class; |
| } else if (type == int.class) { |
| return Integer.class; |
| } else if (type == long.class) { |
| return Long.class; |
| } else if (type == float.class) { |
| return Float.class; |
| } else if (type == double.class) { |
| return Double.class; |
| } else if (type == char.class) { |
| return Character.class; |
| } else if (type == boolean.class) { |
| return Boolean.class; |
| } |
| return type; |
| } |
| } |