All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.checkerframework.common.value.ReflectiveEvaluator Maven / Gradle / Ivy

Go to download

The Checker Framework enhances Java's type system to make it more powerful and useful. This lets software developers detect and prevent errors in their Java programs. The Checker Framework includes compiler plug-ins ("checkers") that find bugs or verify their absence. It also permits you to write your own compiler plug-ins.

There is a newer version: 3.42.0-eisop4
Show newest version
package org.checkerframework.common.value;

import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.NewClassTree;

import org.checkerframework.checker.nullness.qual.Nullable;
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;

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;

// The use of reflection in ReflectiveEvaluator is troubling.
// A static analysis such as the Checker Framework should always use compiler APIs, never
// reflection, to obtain values, for these reasons:
//  * The program being compiled is not necessarily on the classpath nor the processorpath.
//  * There might even be a different class of the same fully-qualified name on the processorpath.
//  * Loading a class can have side effects (say, caused by static initializers).
//
// A better implementation strategy would be to use BeanShell or the like to perform evaluation.

/**
 * 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 final BaseTypeChecker checker;

    /**
     * Whether to report warnings about problems with evaluation. Controlled by the
     * -AreportEvalWarns command-line option.
     */
    private final boolean reportWarnings;

    /**
     * Create a new ReflectiveEvaluator.
     *
     * @param checker the BaseTypeChecker
     * @param factory the annotated type factory
     * @param reportWarnings if true, report warnings about problems with evaluation
     */
    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 lists 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 @Nullable List evaluateMethodCall(
            @Nullable List> allArgValues,
            @Nullable 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 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 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;
    }

    /** An empty Object array. */
    private static final Object[] emptyObjectArray = new Object[] {};

    /**
     * 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 = emptyObjectArray;
        }
        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 = emptyObjectArray;
        }
        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 @Nullable Method getMethodObject(MethodInvocationTree tree) {
        ExecutableElement ele = TreeUtils.elementFromUse(tree);
        List> 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> getParameterClasses(ExecutableElement ele)
            throws ClassNotFoundException {
        return CollectionsPlume.mapList(
                (Element e) -> TypesUtils.getClassFromType(ElementUtils.getType(e)),
                ele.getParameters());
    }

    /**
     * Returns all combinations of the elements of the given lists.
     *
     * @param allArgValues the lists whose cartesian product to form
     * @param whichArg pass {@code allArgValues.size() - 1}
     * @return all combinations of the elements of the given lists
     */
    private List cartesianProduct(List> allArgValues, int whichArg) {
        List argValues = allArgValues.get(whichArg);
        List tuples = new ArrayList<>(argValues.size());

        for (Object value : argValues) {
            if (whichArg == 0) {
                Object[] objects = new Object[allArgValues.size()];
                objects[0] = value;
                tuples.add(objects);
            } else {
                List lastTuples = cartesianProduct(allArgValues, whichArg - 1);
                List 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 copy(List 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. It is a MemberSelectTree or an
     *     IdentifierTree and is used for diagnostics.
     * @return the value of the static field access, or null if it cannot be determined
     */
    public @Nullable 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 exceptions so that the checker doesn't crash.
            if (reportWarnings) {
                checker.reportWarning(
                        tree,
                        "field.access.failed",
                        fieldName,
                        classname,
                        e.getClass() + ": " + e.getMessage());
            }
            return null;
        }
    }

    public @Nullable List evaluteConstructorCall(
            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 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 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> paramClasses = getParameterClasses(ele);
        Class recClass = boxPrimitives(TypesUtils.getClassFromType(typeToCreate));
        Constructor constructor = recClass.getConstructor(paramClasses.toArray(new Class[0]));
        return constructor;
    }

    /**
     * Returns the boxed primitive type if the passed type is an (unboxed) primitive. Otherwise it
     * returns the passed type.
     *
     * @param type a type to box or to return unchanged
     * @return a boxed primitive type, if the argument was primitive; otherwise the argument
     */
    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;
    }
}