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.44.0
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 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> 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 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;
  }

  /**
   * 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> 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());
  }

  private List cartesianProduct(List> allArgValues, int whichArg) {
    List argValues = allArgValues.get(whichArg);
    List tuples = new ArrayList<>();

    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; 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> 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 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;
  }
}