package org.checkerframework.framework.util;

import com.github.javaparser.ParseProblemException;
import com.github.javaparser.ParserConfiguration.LanguageLevel;
import com.github.javaparser.ast.ArrayCreationLevel;
import com.github.javaparser.ast.expr.ArrayAccessExpr;
import com.github.javaparser.ast.expr.ArrayCreationExpr;
import com.github.javaparser.ast.expr.BinaryExpr;
import com.github.javaparser.ast.expr.BooleanLiteralExpr;
import com.github.javaparser.ast.expr.CharLiteralExpr;
import com.github.javaparser.ast.expr.ClassExpr;
import com.github.javaparser.ast.expr.DoubleLiteralExpr;
import com.github.javaparser.ast.expr.EnclosedExpr;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.FieldAccessExpr;
import com.github.javaparser.ast.expr.IntegerLiteralExpr;
import com.github.javaparser.ast.expr.LongLiteralExpr;
import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.ast.expr.NameExpr;
import com.github.javaparser.ast.expr.NullLiteralExpr;
import com.github.javaparser.ast.expr.StringLiteralExpr;
import com.github.javaparser.ast.expr.SuperExpr;
import com.github.javaparser.ast.expr.ThisExpr;
import com.github.javaparser.ast.expr.UnaryExpr;
import com.github.javaparser.ast.type.Type;
import com.github.javaparser.ast.visitor.GenericVisitorWithDefaults;
import com.sun.source.tree.Tree;
import com.sun.source.util.TreePath;
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.Symbol.PackageSymbol;
import com.sun.tools.javac.code.Type.ArrayType;
import com.sun.tools.javac.code.Type.ClassType;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.regex.qual.Regex;
import org.checkerframework.dataflow.expression.ArrayAccess;
import org.checkerframework.dataflow.expression.ArrayCreation;
import org.checkerframework.dataflow.expression.BinaryOperation;
import org.checkerframework.dataflow.expression.ClassName;
import org.checkerframework.dataflow.expression.FieldAccess;
import org.checkerframework.dataflow.expression.FormalParameter;
import org.checkerframework.dataflow.expression.JavaExpression;
import org.checkerframework.dataflow.expression.LocalVariable;
import org.checkerframework.dataflow.expression.MethodCall;
import org.checkerframework.dataflow.expression.SuperReference;
import org.checkerframework.dataflow.expression.ThisReference;
import org.checkerframework.dataflow.expression.UnaryOperation;
import org.checkerframework.dataflow.expression.ValueLiteral;
import org.checkerframework.framework.source.DiagMessage;
import org.checkerframework.framework.util.dependenttypes.DependentTypesError;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.ElementUtils;
import org.checkerframework.javacutil.Resolver;
import org.checkerframework.javacutil.TypesUtils;
import org.checkerframework.javacutil.trees.TreeBuilder;
import org.plumelib.util.CollectionsPlume;
import org.plumelib.util.StringsPlume;

/**
 * Helper methods to parse a string that represents a restricted Java expression.
 *
 * @checker_framework.manual #java-expressions-as-arguments Writing Java expressions as annotation
 *     arguments
 * @checker_framework.manual #dependent-types Annotations whose argument is a Java expression
 *     (dependent type annotations)
 */
public class JavaExpressionParseUtil {

  /** Regular expression for a formal parameter use. */
  protected static final String PARAMETER_REGEX = "#([1-9][0-9]*)";

  /**
   * Anchored pattern for a formal parameter use; matches a string that is exactly a formal
   * parameter use.
   */
  protected static final @Regex(1) Pattern ANCHORED_PARAMETER_PATTERN =
      Pattern.compile("^" + PARAMETER_REGEX + "$");

  /**
   * Unanchored pattern for a formal parameter use; can be used to find all formal parameter uses.
   */
  protected static final @Regex(1) Pattern UNANCHORED_PARAMETER_PATTERN =
      Pattern.compile(PARAMETER_REGEX);

  /**
   * Parsable replacement for formal parameter references. It is parsable because it is a Java
   * identifier.
   */
  private static final String PARAMETER_PREFIX = "_param_";

  /** The length of {@link #PARAMETER_PREFIX}. */
  private static final int PARAMETER_PREFIX_LENGTH = PARAMETER_PREFIX.length();

  /** A pattern that matches the start of a formal parameter in "#2" syntax. */
  private static final Pattern FORMAL_PARAMETER = Pattern.compile("#(\\d)");

  /** The replacement for a formal parameter in "#2" syntax. */
  private static final String PARAMETER_REPLACEMENT = PARAMETER_PREFIX + "$1";

  /**
   * Parses a string to a {@link JavaExpression}.
   *
   * <p>For most uses, clients should call one of the static methods in {@link
   * StringToJavaExpression} rather than calling this method directly.
   *
   * @param expression the string expression to parse
   * @param enclosingType type of the class that encloses the JavaExpression
   * @param thisReference the JavaExpression to which to parse "this", or null if "this" should not
   *     appear in the expression
   * @param parameters list of JavaExpressions to which to parse formal parameter references such as
   *     "#2", or null if formal parameter references should not appear in the expression
   * @param localVarPath if non-null, the expression is parsed as if it were written at this
   *     location; affects only parsing of local variables
   * @param pathToCompilationUnit required to use the underlying Javac API
   * @param env the processing environment
   * @return {@code expression} as a {@code JavaExpression}
   * @throws JavaExpressionParseException if the string cannot be parsed
   */
  public static JavaExpression parse(
      String expression,
      TypeMirror enclosingType,
      @Nullable ThisReference thisReference,
      @Nullable List<FormalParameter> parameters,
      @Nullable TreePath localVarPath,
      TreePath pathToCompilationUnit,
      ProcessingEnvironment env)
      throws JavaExpressionParseException {

    // Use the current source version to parse with because a JavaExpression could refer to a
    // variable named "var", which is a keyword in Java 10 and later.
    LanguageLevel currentSourceVersion = JavaParserUtil.getCurrentSourceVersion(env);
    String expressionWithParameterNames =
        StringsPlume.replaceAll(expression, FORMAL_PARAMETER, PARAMETER_REPLACEMENT);
    Expression expr;
    try {
      expr = JavaParserUtil.parseExpression(expressionWithParameterNames, currentSourceVersion);
    } catch (ParseProblemException e) {
      String extra = ".";
      if (!e.getProblems().isEmpty()) {
        String message = e.getProblems().get(0).getMessage();
        int newLine = message.indexOf(System.lineSeparator());
        if (newLine != -1) {
          message = message.substring(0, newLine);
        }
        extra = ". Error message: " + message;
      }
      throw constructJavaExpressionParseError(expression, "the expression did not parse" + extra);
    }

    JavaExpression result =
        ExpressionToJavaExpressionVisitor.convert(
            expr,
            enclosingType,
            thisReference,
            parameters,
            localVarPath,
            pathToCompilationUnit,
            env);

    if (result instanceof ClassName && !expression.endsWith(".class")) {
      throw constructJavaExpressionParseError(
          expression,
          String.format(
              "a class name cannot terminate a Java expression string, where result=%s [%s]",
              result, result.getClass()));
    }
    return result;
  }

  /**
   * A visitor class that converts a JavaParser {@link Expression} to a {@link JavaExpression}. This
   * class does not viewpoint-adapt the expression.
   */
  private static class ExpressionToJavaExpressionVisitor
      extends GenericVisitorWithDefaults<JavaExpression, Void> {

    /**
     * The underlying javac API used to convert from Strings to Elements requires a tree path even
     * when the information could be deduced from elements alone. So use the path to the current
     * CompilationUnit.
     */
    private final TreePath pathToCompilationUnit;

    /** If non-null, the expression is parsed as if it were written at this location. */
    private final @Nullable TreePath localVarPath;

    /** The processing environment. */
    private final ProcessingEnvironment env;

    /** The resolver. Computed from the environment, but lazily initialized. */
    private @MonotonicNonNull Resolver resolver = null;

    /** The type utilities. */
    private final Types types;

    /** The java.lang.String type. */
    private final TypeMirror stringTypeMirror;

    /** The enclosing type. Used to look up unqualified method, field, and class names. */
    private final TypeMirror enclosingType;

    /**
     * The expression to use for "this". If {@code null}, a parse error will be thrown if "this"
     * appears in the expression.
     */
    private final @Nullable ThisReference thisReference;

    /**
     * For each formal parameter, the expression to which to parse it. For example, the second
     * (index 1) element of the list is what "#2" parses to. If this field is {@code null}, a parse
     * error will be thrown if "#2" appears in the expression.
     */
    private final @Nullable List<FormalParameter> parameters;

    /**
     * Create a new ExpressionToJavaExpressionVisitor.
     *
     * @param enclosingType type of the class that encloses the JavaExpression
     * @param thisReference a JavaExpression to which to parse "this", or null if "this" should not
     *     appear in the expression
     * @param parameters list of JavaExpressions to which to parse a formal parameter reference such
     *     as "#2", or null if parameters should not appear in the expression
     * @param localVarPath if non-null, the expression is parsed as if it were written at this
     *     location
     * @param pathToCompilationUnit required to use the underlying Javac API
     * @param env the processing environment
     */
    private ExpressionToJavaExpressionVisitor(
        TypeMirror enclosingType,
        @Nullable ThisReference thisReference,
        @Nullable List<FormalParameter> parameters,
        @Nullable TreePath localVarPath,
        TreePath pathToCompilationUnit,
        ProcessingEnvironment env) {
      this.pathToCompilationUnit = pathToCompilationUnit;
      this.localVarPath = localVarPath;
      this.env = env;
      this.types = env.getTypeUtils();
      this.stringTypeMirror = env.getElementUtils().getTypeElement("java.lang.String").asType();
      this.enclosingType = enclosingType;
      this.thisReference = thisReference;
      this.parameters = parameters;
    }

    /**
     * Converts a JavaParser {@link Expression} to a {@link JavaExpression}.
     *
     * @param expr the JavaParser {@link Expression} to convert
     * @param enclosingType type of the class that encloses the JavaExpression
     * @param thisReference a JavaExpression to which to parse "this", or null if "this" should not
     *     appear in the expression
     * @param parameters list of JavaExpressions to which to parse parameters, or null if parameters
     *     should not appear in the expression
     * @param localVarPath if non-null, the expression is parsed as if it were written at this
     *     location
     * @param pathToCompilationUnit required to use the underlying Javac API
     * @param env the processing environment
     * @return {@code expr} as a {@code JavaExpression}
     * @throws JavaExpressionParseException if {@code expr} cannot be converted to a {@code
     *     JavaExpression}
     */
    public static JavaExpression convert(
        Expression expr,
        TypeMirror enclosingType,
        @Nullable ThisReference thisReference,
        @Nullable List<FormalParameter> parameters,
        @Nullable TreePath localVarPath,
        TreePath pathToCompilationUnit,
        ProcessingEnvironment env)
        throws JavaExpressionParseException {
      try {
        return expr.accept(
            new ExpressionToJavaExpressionVisitor(
                enclosingType, thisReference, parameters, localVarPath, pathToCompilationUnit, env),
            null);
      } catch (ParseRuntimeException e) {
        // Convert unchecked to checked exception. Visitor methods can't throw checked
        // exceptions. They override the methods in the superclass, and a checked exception
        // would change the method signature.
        throw e.getCheckedException();
      }
    }

    /**
     * Initializes the {@code resolver} field if necessary. Does nothing on invocations after the
     * first.
     */
    @EnsuresNonNull("resolver")
    private void setResolverField() {
      if (resolver == null) {
        resolver = new Resolver(env);
      }
    }

    /** If the expression is not supported, throw a {@link ParseRuntimeException} by default. */
    @Override
    public JavaExpression defaultAction(com.github.javaparser.ast.Node n, Void aVoid) {
      throw new ParseRuntimeException(
          constructJavaExpressionParseError(
              n.toString(), n.getClass() + " is not a supported expression"));
    }

    @Override
    public JavaExpression visit(NullLiteralExpr expr, Void aVoid) {
      return new ValueLiteral(types.getNullType(), (Object) null);
    }

    @Override
    public JavaExpression visit(IntegerLiteralExpr expr, Void aVoid) {
      return new ValueLiteral(types.getPrimitiveType(TypeKind.INT), expr.asNumber());
    }

    @Override
    public JavaExpression visit(LongLiteralExpr expr, Void aVoid) {
      return new ValueLiteral(types.getPrimitiveType(TypeKind.LONG), expr.asNumber());
    }

    @Override
    public JavaExpression visit(CharLiteralExpr expr, Void aVoid) {
      return new ValueLiteral(types.getPrimitiveType(TypeKind.CHAR), expr.asChar());
    }

    @Override
    public JavaExpression visit(DoubleLiteralExpr expr, Void aVoid) {
      return new ValueLiteral(types.getPrimitiveType(TypeKind.DOUBLE), expr.asDouble());
    }

    @Override
    public JavaExpression visit(StringLiteralExpr expr, Void aVoid) {
      return new ValueLiteral(stringTypeMirror, expr.asString());
    }

    @Override
    public JavaExpression visit(BooleanLiteralExpr expr, Void aVoid) {
      return new ValueLiteral(types.getPrimitiveType(TypeKind.BOOLEAN), expr.getValue());
    }

    @Override
    public JavaExpression visit(ThisExpr n, Void aVoid) {
      if (thisReference == null) {
        throw new ParseRuntimeException(
            constructJavaExpressionParseError("this", "\"this\" isn't allowed here"));
      }
      return thisReference;
    }

    @Override
    public JavaExpression visit(SuperExpr n, Void aVoid) {
      if (thisReference == null) {
        throw new ParseRuntimeException(
            constructJavaExpressionParseError("super", "\"super\" isn't allowed here"));
      }
      // super literal
      TypeMirror superclass = TypesUtils.getSuperclass(enclosingType, types);
      if (superclass == null) {
        throw new ParseRuntimeException(
            constructJavaExpressionParseError("super", enclosingType + " has no superclass"));
      }
      return new SuperReference(superclass);
    }

    // expr is an expression in parentheses.
    @Override
    public JavaExpression visit(EnclosedExpr expr, Void aVoid) {
      return expr.getInner().accept(this, null);
    }

    @Override
    public JavaExpression visit(ArrayAccessExpr expr, Void aVoid) {
      JavaExpression array = expr.getName().accept(this, null);
      TypeMirror arrayType = array.getType();
      if (arrayType.getKind() != TypeKind.ARRAY) {
        throw new ParseRuntimeException(
            constructJavaExpressionParseError(
                expr.toString(),
                String.format(
                    "expected an array, found %s of type %s [%s]",
                    array, arrayType, arrayType.getKind())));
      }
      TypeMirror componentType = ((ArrayType) arrayType).getComponentType();

      JavaExpression index = expr.getIndex().accept(this, null);

      return new ArrayAccess(componentType, array, index);
    }

    // expr is an identifier with no dots in its name.
    @Override
    public JavaExpression visit(NameExpr expr, Void aVoid) {
      String s = expr.getNameAsString();
      setResolverField();

      // Formal parameter, using "#2" syntax.
      JavaExpression parameter = getParameterJavaExpression(s);
      if (parameter != null) {
        // A parameter is a local variable, but it can be referenced outside of local scope
        // (at the method scope) using the special #NN syntax.
        return parameter;
      }

      // Local variable or parameter.
      if (localVarPath != null) {
        // Attempt to match a local variable within the scope of the
        // given path before attempting to match a field.
        VariableElement varElem = resolver.findLocalVariableOrParameter(s, localVarPath);
        if (varElem != null) {
          return new LocalVariable(varElem);
        }
      }

      // Field access
      JavaExpression fieldAccessReceiver;
      if (thisReference != null) {
        fieldAccessReceiver = thisReference;
      } else {
        fieldAccessReceiver = new ClassName(enclosingType);
      }
      FieldAccess fieldAccess = getIdentifierAsFieldAccess(fieldAccessReceiver, s);
      if (fieldAccess != null) {
        return fieldAccess;
      }

      if (localVarPath != null) {
        Element classElem = resolver.findClass(s, localVarPath);
        TypeMirror classType = ElementUtils.getType(classElem);
        if (classType != null) {
          return new ClassName(classType);
        }
      }

      ClassName classType = getIdentifierAsUnqualifiedClassName(s);
      if (classType != null) {
        return classType;
      }

      // Err if a formal parameter name is used, instead of the "#2" syntax.
      if (parameters != null) {
        for (int i = 0; i < parameters.size(); i++) {
          Element varElt = parameters.get(i).getElement();
          if (varElt.getSimpleName().contentEquals(s)) {
            throw new ParseRuntimeException(
                constructJavaExpressionParseError(
                    s, String.format(DependentTypesError.FORMAL_PARAM_NAME_STRING, i + 1, s)));
          }
        }
      }

      throw new ParseRuntimeException(constructJavaExpressionParseError(s, "identifier not found"));
    }

    /**
     * If {@code s} a parameter expressed using the {@code #NN} syntax, then returns a
     * JavaExpression for the given parameter; that is, returns an element of {@code parameters}.
     * Otherwise, returns {@code null}.
     *
     * @param s a String that starts with PARAMETER_PREFIX
     * @return the JavaExpression for the given parameter or {@code null} if {@code s} is not a
     *     parameter
     */
    private @Nullable JavaExpression getParameterJavaExpression(String s) {
      if (!s.startsWith(PARAMETER_PREFIX)) {
        return null;
      }
      if (parameters == null) {
        throw new ParseRuntimeException(
            constructJavaExpressionParseError(s, "no parameters found"));
      }
      int idx = Integer.parseInt(s.substring(PARAMETER_PREFIX_LENGTH));

      if (idx == 0) {
        throw new ParseRuntimeException(
            constructJavaExpressionParseError(
                "#0", "Use \"this\" for the receiver or \"#1\" for the first formal parameter"));
      }
      if (idx > parameters.size()) {
        throw new ParseRuntimeException(
            new JavaExpressionParseException(
                "flowexpr.parse.index.too.big", Integer.toString(idx)));
      }
      return parameters.get(idx - 1);
    }

    /**
     * If {@code identifier} is the simple class name of any inner class of {@code type}, return the
     * {@link ClassName} for the inner class. If not, return null.
     *
     * @param type type to search for {@code identifier}
     * @param identifier possible simple class name
     * @return the {@code ClassName} for {@code identifier}, or null if it is not a simple class
     *     name
     */
    protected @Nullable ClassName getIdentifierAsInnerClassName(
        TypeMirror type, String identifier) {
      if (type.getKind() != TypeKind.DECLARED) {
        return null;
      }

      Element outerClass = ((DeclaredType) type).asElement();
      for (Element memberElement : outerClass.getEnclosedElements()) {
        if (!(memberElement.getKind().isClass() || memberElement.getKind().isInterface())) {
          continue;
        }
        if (memberElement.getSimpleName().contentEquals(identifier)) {
          return new ClassName(ElementUtils.getType(memberElement));
        }
      }
      return null;
    }

    /**
     * If {@code identifier} is a class name with that can be referenced using only its simple name
     * within {@code enclosingType}, return the {@link ClassName} for the class. If not, return
     * null.
     *
     * <p>{@code identifier} may be
     *
     * <ol>
     *   <li>the simple name of {@code type}.
     *   <li>the simple name of a class declared in {@code type} or in an enclosing type of {@code
     *       type}.
     *   <li>the simple name of a class in the java.lang package.
     *   <li>the simple name of a class in the unnamed package.
     * </ol>
     *
     * @param identifier possible class name
     * @return the {@code ClassName} for {@code identifier}, or null if it is not a class name
     */
    protected @Nullable ClassName getIdentifierAsUnqualifiedClassName(String identifier) {
      // Is identifier an inner class of enclosingType or of any enclosing class of
      // enclosingType?
      TypeMirror searchType = enclosingType;
      while (searchType.getKind() == TypeKind.DECLARED) {
        DeclaredType searchDeclaredType = (DeclaredType) searchType;
        if (searchDeclaredType.asElement().getSimpleName().contentEquals(identifier)) {
          return new ClassName(searchType);
        }
        ClassName className = getIdentifierAsInnerClassName(searchType, identifier);
        if (className != null) {
          return className;
        }
        searchType = getTypeOfEnclosingClass(searchDeclaredType);
      }

      setResolverField();

      if (enclosingType.getKind() == TypeKind.DECLARED) {
        // Is identifier in the same package as this?
        PackageSymbol packageSymbol =
            (PackageSymbol)
                ElementUtils.enclosingPackage(((DeclaredType) enclosingType).asElement());
        ClassSymbol classSymbol =
            resolver.findClassInPackage(identifier, packageSymbol, pathToCompilationUnit);
        if (classSymbol != null) {
          return new ClassName(classSymbol.asType());
        }
      }
      // Is identifier a simple name for a class in java.lang?
      Symbol.PackageSymbol packageSymbol = resolver.findPackage("java.lang", pathToCompilationUnit);
      if (packageSymbol == null) {
        throw new BugInCF("Can't find java.lang package.");
      }
      ClassSymbol classSymbol =
          resolver.findClassInPackage(identifier, packageSymbol, pathToCompilationUnit);
      if (classSymbol != null) {
        return new ClassName(classSymbol.asType());
      }

      // Is identifier a class in the unnamed package?
      Element classElem = resolver.findClass(identifier, pathToCompilationUnit);
      if (classElem != null) {
        PackageElement pkg = ElementUtils.enclosingPackage(classElem);
        if (pkg != null && pkg.isUnnamed()) {
          TypeMirror classType = ElementUtils.getType(classElem);
          if (classType != null) {
            return new ClassName(classType);
          }
        }
      }

      return null;
    }

    /**
     * Return the {@link FieldAccess} expression for the field with name {@code identifier} accessed
     * via {@code receiverExpr}. If no such field exists, then {@code null} is returned.
     *
     * @param receiverExpr the receiver of the field access; the expression used to access the field
     * @param identifier possibly a field name
     * @return a field access, or null if {@code identifier} is not a field that can be accessed via
     *     {@code receiverExpr}
     */
    protected @Nullable FieldAccess getIdentifierAsFieldAccess(
        JavaExpression receiverExpr, String identifier) {
      setResolverField();
      // Find the field element.
      TypeMirror enclosingTypeOfField = receiverExpr.getType();
      VariableElement fieldElem;
      if (identifier.equals("length") && enclosingTypeOfField.getKind() == TypeKind.ARRAY) {
        fieldElem = resolver.findField(identifier, enclosingTypeOfField, pathToCompilationUnit);
        if (fieldElem == null) {
          throw new BugInCF("length field not found for type %s", enclosingTypeOfField);
        }
      } else {
        fieldElem = null;
        // Search for field in each enclosing class.
        while (enclosingTypeOfField.getKind() == TypeKind.DECLARED) {
          fieldElem = resolver.findField(identifier, enclosingTypeOfField, pathToCompilationUnit);
          if (fieldElem != null) {
            break;
          }
          enclosingTypeOfField = getTypeOfEnclosingClass((DeclaredType) enclosingTypeOfField);
        }
        if (fieldElem == null) {
          // field not found.
          return null;
        }
        if (receiverExpr instanceof SuperReference
            && thisReference.getType().getKind() == TypeKind.DECLARED) {
          Element thisFieldElem =
              resolver.findField(identifier, thisReference.getType(), pathToCompilationUnit);
          if (thisFieldElem == null) {
            receiverExpr = thisReference;
          }
        }
      }

      // `fieldElem` is now set.  Construct a FieldAccess expression.

      if (ElementUtils.isStatic(fieldElem)) {
        Element classElem = fieldElem.getEnclosingElement();
        JavaExpression staticClassReceiver = new ClassName(ElementUtils.getType(classElem));
        return new FieldAccess(staticClassReceiver, fieldElem);
      }

      // fieldElem is an instance field.

      if (receiverExpr instanceof ClassName) {
        throw new ParseRuntimeException(
            constructJavaExpressionParseError(
                fieldElem.getSimpleName().toString(),
                "a non-static field cannot have a class name as a receiver."));
      }

      // There are two possibilities, captured by local variable fieldDeclaredInReceiverType:
      //  * true: it's an instance field declared in the type (or supertype) of receiverExpr.
      //  * false: it's an instance field declared in an enclosing type of receiverExpr.

      @SuppressWarnings("interning:not.interned") // Checking for exact object
      boolean fieldDeclaredInReceiverType = enclosingTypeOfField == receiverExpr.getType();
      if (fieldDeclaredInReceiverType) {
        TypeMirror fieldType = ElementUtils.getType(fieldElem);
        return new FieldAccess(receiverExpr, fieldType, fieldElem);
      } else {
        if (!(receiverExpr instanceof ThisReference)) {
          String msg =
              String.format(
                  "%s is declared in an outer type of the type of the receiver expression, %s.",
                  identifier, receiverExpr);
          throw new ParseRuntimeException(constructJavaExpressionParseError(identifier, msg));
        }
        TypeElement receiverTypeElement = TypesUtils.getTypeElement(receiverExpr.getType());
        if (receiverTypeElement == null || ElementUtils.isStatic(receiverTypeElement)) {
          String msg =
              String.format("%s is a non-static field declared in an outer type this.", identifier);
          throw new ParseRuntimeException(constructJavaExpressionParseError(identifier, msg));
        }
        JavaExpression locationOfField = new ThisReference(enclosingTypeOfField);
        return new FieldAccess(locationOfField, fieldElem);
      }
    }

    @Override
    public JavaExpression visit(MethodCallExpr expr, Void aVoid) {
      setResolverField();

      JavaExpression receiverExpr;
      if (expr.getScope().isPresent()) {
        receiverExpr = expr.getScope().get().accept(this, null);
        expr = expr.removeScope();
      } else if (thisReference != null) {
        receiverExpr = thisReference;
      } else {
        receiverExpr = new ClassName(enclosingType);
      }

      String methodName = expr.getNameAsString();

      // parse argument list
      List<JavaExpression> arguments =
          CollectionsPlume.mapList(argument -> argument.accept(this, null), expr.getArguments());

      ExecutableElement methodElement;
      try {
        methodElement =
            getMethodElement(
                methodName, receiverExpr.getType(), pathToCompilationUnit, arguments, resolver);
      } catch (JavaExpressionParseException e) {
        throw new ParseRuntimeException(e);
      }

      // Box any arguments that require it.
      for (int i = 0; i < arguments.size(); i++) {
        VariableElement parameter = methodElement.getParameters().get(i);
        TypeMirror parameterType = parameter.asType();
        JavaExpression argument = arguments.get(i);
        TypeMirror argumentType = argument.getType();
        // is boxing necessary?
        if (TypesUtils.isBoxedPrimitive(parameterType) && TypesUtils.isPrimitive(argumentType)) {
          // boxing is necessary
          MethodSymbol valueOfMethod = TreeBuilder.getValueOfMethod(env, parameterType);
          JavaExpression boxedParam =
              new MethodCall(
                  parameterType,
                  valueOfMethod,
                  new ClassName(parameterType),
                  Collections.singletonList(argument));
          arguments.set(i, boxedParam);
        }
      }

      // Build the MethodCall expression object.
      if (ElementUtils.isStatic(methodElement)) {
        Element classElem = methodElement.getEnclosingElement();
        JavaExpression staticClassReceiver = new ClassName(ElementUtils.getType(classElem));
        return new MethodCall(
            ElementUtils.getType(methodElement), methodElement, staticClassReceiver, arguments);
      } else {
        if (receiverExpr instanceof ClassName) {
          throw new ParseRuntimeException(
              constructJavaExpressionParseError(
                  expr.toString(),
                  "a non-static method call cannot have a class name as a receiver"));
        }
        TypeMirror methodType =
            TypesUtils.substituteMethodReturnType(methodElement, receiverExpr.getType(), env);
        return new MethodCall(methodType, methodElement, receiverExpr, arguments);
      }
    }

    /**
     * Returns the ExecutableElement for a method, or throws an exception.
     *
     * <p>(This method takes into account autoboxing.)
     *
     * @param methodName the method name
     * @param receiverType the receiver type
     * @param pathToCompilationUnit the path to the compilation unit
     * @param arguments the arguments
     * @param resolver the resolver
     * @return the ExecutableElement for a method, or throws an exception
     * @throws JavaExpressionParseException if the string cannot be parsed as a method name
     */
    private ExecutableElement getMethodElement(
        String methodName,
        TypeMirror receiverType,
        TreePath pathToCompilationUnit,
        List<JavaExpression> arguments,
        Resolver resolver)
        throws JavaExpressionParseException {

      List<TypeMirror> argumentTypes = CollectionsPlume.mapList(JavaExpression::getType, arguments);

      if (receiverType.getKind() == TypeKind.ARRAY) {
        ExecutableElement element =
            resolver.findMethod(methodName, receiverType, pathToCompilationUnit, argumentTypes);
        if (element == null) {
          throw constructJavaExpressionParseError(methodName, "no such method");
        }
        return element;
      }

      // Search for method in each enclosing class.
      while (receiverType.getKind() == TypeKind.DECLARED) {
        ExecutableElement element =
            resolver.findMethod(methodName, receiverType, pathToCompilationUnit, argumentTypes);
        if (element != null) {
          return element;
        }
        receiverType = getTypeOfEnclosingClass((DeclaredType) receiverType);
      }

      // Method not found.
      throw constructJavaExpressionParseError(methodName, "no such method");
    }

    // `expr` should be a field access, a fully qualified class name, or a class name qualified
    // with another class name (e.g. {@code OuterClass.InnerClass}).  If the expression refers
    // to a class that is not available to the resolver (the class wasn't passed to javac on
    // the command line), then the argument can be "outerpackage.innerpackage", which will lead
    // to a confusing error message.
    @Override
    public JavaExpression visit(FieldAccessExpr expr, Void aVoid) {
      setResolverField();

      Expression scope = expr.getScope();
      String name = expr.getNameAsString();

      // Check for fully qualified class name.
      Symbol.PackageSymbol packageSymbol =
          resolver.findPackage(scope.toString(), pathToCompilationUnit);
      if (packageSymbol != null) {
        ClassSymbol classSymbol =
            resolver.findClassInPackage(name, packageSymbol, pathToCompilationUnit);
        if (classSymbol != null) {
          return new ClassName(classSymbol.asType());
        }
        throw new ParseRuntimeException(
            constructJavaExpressionParseError(
                expr.toString(),
                "could not find class "
                    + expr.getNameAsString()
                    + " in package "
                    + scope.toString()));
      }

      JavaExpression receiver = scope.accept(this, null);

      // Check for field access expression.
      FieldAccess fieldAccess = getIdentifierAsFieldAccess(receiver, name);
      if (fieldAccess != null) {
        return fieldAccess;
      }

      // Check for inner class.
      ClassName classType = getIdentifierAsInnerClassName(receiver.getType(), name);
      if (classType != null) {
        return classType;
      }

      throw new ParseRuntimeException(
          constructJavaExpressionParseError(
              name, String.format("field or class %s not found in %s", name, receiver)));
    }

    // expr is a Class literal
    @Override
    public JavaExpression visit(ClassExpr expr, Void aVoid) {
      TypeMirror result = convertTypeToTypeMirror(expr.getType());
      if (result == null) {
        throw new ParseRuntimeException(
            constructJavaExpressionParseError(
                expr.toString(), "it is an unparsable class literal"));
      }
      return new ClassName(result);
    }

    @Override
    public JavaExpression visit(ArrayCreationExpr expr, Void aVoid) {
      List<@Nullable JavaExpression> dimensions =
          CollectionsPlume.mapList(
              (ArrayCreationLevel dimension) ->
                  dimension.getDimension().map(dim -> dim.accept(this, aVoid)).orElse(null),
              expr.getLevels());

      List<JavaExpression> initializers;
      if (expr.getInitializer().isPresent()) {
        initializers =
            CollectionsPlume.mapList(
                (Expression initializer) -> initializer.accept(this, null),
                expr.getInitializer().get().getValues());
      } else {
        initializers = Collections.emptyList();
      }
      TypeMirror arrayType = convertTypeToTypeMirror(expr.getElementType());
      if (arrayType == null) {
        throw new ParseRuntimeException(
            constructJavaExpressionParseError(
                expr.getElementType().asString(), "type not parsable"));
      }
      for (int i = 0; i < dimensions.size(); i++) {
        arrayType = TypesUtils.createArrayType(arrayType, env.getTypeUtils());
      }
      return new ArrayCreation(arrayType, dimensions, initializers);
    }

    @Override
    public JavaExpression visit(UnaryExpr expr, Void aVoid) {
      Tree.Kind treeKind = javaParserUnaryOperatorToTreeKind(expr.getOperator());
      JavaExpression operand = expr.getExpression().accept(this, null);
      // This eliminates + and performs constant-folding for -; it could also do so for other
      // operations.
      switch (treeKind) {
        case UNARY_PLUS:
          return operand;
        case UNARY_MINUS:
          if (operand instanceof ValueLiteral) {
            return ((ValueLiteral) operand).negate();
          }
          break;
        default:
          // Not optimization for this operand
          break;
      }
      return new UnaryOperation(operand.getType(), treeKind, operand);
    }

    /**
     * Convert a JavaParser unary operator to a TreeKind.
     *
     * @param op a JavaParser unary operator
     * @return a TreeKind for the unary operator
     */
    private Tree.Kind javaParserUnaryOperatorToTreeKind(UnaryExpr.Operator op) {
      switch (op) {
        case BITWISE_COMPLEMENT:
          return Tree.Kind.BITWISE_COMPLEMENT;
        case LOGICAL_COMPLEMENT:
          return Tree.Kind.LOGICAL_COMPLEMENT;
        case MINUS:
          return Tree.Kind.UNARY_MINUS;
        case PLUS:
          return Tree.Kind.UNARY_PLUS;
        case POSTFIX_DECREMENT:
          return Tree.Kind.POSTFIX_DECREMENT;
        case POSTFIX_INCREMENT:
          return Tree.Kind.POSTFIX_INCREMENT;
        case PREFIX_DECREMENT:
          return Tree.Kind.PREFIX_DECREMENT;
        case PREFIX_INCREMENT:
          return Tree.Kind.PREFIX_INCREMENT;
        default:
          throw new BugInCF("unhandled " + op);
      }
    }

    @Override
    public JavaExpression visit(BinaryExpr expr, Void aVoid) {
      JavaExpression leftJe = expr.getLeft().accept(this, null);
      JavaExpression rightJe = expr.getRight().accept(this, null);
      TypeMirror leftType = leftJe.getType();
      TypeMirror rightType = rightJe.getType();
      TypeMirror type;
      // isSubtype() first does the cheaper test isSameType(), so no need to do it here.
      if (expr.getOperator() == BinaryExpr.Operator.PLUS
          && (TypesUtils.isString(leftType) || TypesUtils.isString(rightType))) {
        // JLS 15.18.1 says, "If only one operand expression is of type String, then string
        // conversion is performed on the other operand to produce a string at run time."
        type = stringTypeMirror;
      } else if (types.isSubtype(leftType, rightType)) {
        type = rightType;
      } else if (types.isSubtype(rightType, leftType)) {
        type = leftType;
      } else {
        throw new ParseRuntimeException(
            constructJavaExpressionParseError(
                expr.toString(),
                String.format("inconsistent types %s %s for %s", leftType, rightType, expr)));
      }
      return new BinaryOperation(
          type, javaParserBinaryOperatorToTreeKind(expr.getOperator()), leftJe, rightJe);
    }

    /**
     * Convert a JavaParser binary operator to a TreeKind.
     *
     * @param op a JavaParser binary operator
     * @return a TreeKind for the binary operator
     */
    private Tree.Kind javaParserBinaryOperatorToTreeKind(BinaryExpr.Operator op) {
      switch (op) {
        case AND:
          return Tree.Kind.CONDITIONAL_AND;
        case BINARY_AND:
          return Tree.Kind.AND;
        case BINARY_OR:
          return Tree.Kind.OR;
        case DIVIDE:
          return Tree.Kind.DIVIDE;
        case EQUALS:
          return Tree.Kind.EQUAL_TO;
        case GREATER:
          return Tree.Kind.GREATER_THAN;
        case GREATER_EQUALS:
          return Tree.Kind.GREATER_THAN_EQUAL;
        case LEFT_SHIFT:
          return Tree.Kind.LEFT_SHIFT;
        case LESS:
          return Tree.Kind.LESS_THAN;
        case LESS_EQUALS:
          return Tree.Kind.LESS_THAN_EQUAL;
        case MINUS:
          return Tree.Kind.MINUS;
        case MULTIPLY:
          return Tree.Kind.MULTIPLY;
        case NOT_EQUALS:
          return Tree.Kind.NOT_EQUAL_TO;
        case OR:
          return Tree.Kind.CONDITIONAL_OR;
        case PLUS:
          return Tree.Kind.PLUS;
        case REMAINDER:
          return Tree.Kind.REMAINDER;
        case SIGNED_RIGHT_SHIFT:
          return Tree.Kind.RIGHT_SHIFT;
        case UNSIGNED_RIGHT_SHIFT:
          return Tree.Kind.UNSIGNED_RIGHT_SHIFT;
        case XOR:
          return Tree.Kind.XOR;
        default:
          throw new BugInCF("unhandled " + op);
      }
    }

    /**
     * Converts the JavaParser type to a TypeMirror. Returns null if {@code type} is not handled;
     * this method does not handle type variables, union types, or intersection types.
     *
     * @param type a JavaParser type
     * @return a TypeMirror corresponding to {@code type}, or null if {@code type} isn't handled
     */
    private @Nullable TypeMirror convertTypeToTypeMirror(Type type) {
      if (type.isClassOrInterfaceType()) {
        LanguageLevel currentSourceVersion = JavaParserUtil.getCurrentSourceVersion(env);
        try {
          return JavaParserUtil.parseExpression(type.asString(), currentSourceVersion)
              .accept(this, null)
              .getType();
        } catch (ParseProblemException e) {
          return null;
        }
      } else if (type.isPrimitiveType()) {
        switch (type.asPrimitiveType().getType()) {
          case BOOLEAN:
            return types.getPrimitiveType(TypeKind.BOOLEAN);
          case BYTE:
            return types.getPrimitiveType(TypeKind.BYTE);
          case SHORT:
            return types.getPrimitiveType(TypeKind.SHORT);
          case INT:
            return types.getPrimitiveType(TypeKind.INT);
          case CHAR:
            return types.getPrimitiveType(TypeKind.CHAR);
          case FLOAT:
            return types.getPrimitiveType(TypeKind.FLOAT);
          case LONG:
            return types.getPrimitiveType(TypeKind.LONG);
          case DOUBLE:
            return types.getPrimitiveType(TypeKind.DOUBLE);
        }
      } else if (type.isVoidType()) {
        return types.getNoType(TypeKind.VOID);
      } else if (type.isArrayType()) {
        TypeMirror componentType = convertTypeToTypeMirror(type.asArrayType().getComponentType());
        if (componentType == null) {
          return null;
        }
        return types.getArrayType(componentType);
      }
      return null;
    }
  }

  /**
   * If {@code s} is exactly a formal parameter, return its 1-based index. Returns -1 otherwise.
   *
   * @param s a Java expression
   * @return the 1-based index of the formal parameter that {@code s} represents, or -1
   */
  public static int parameterIndex(String s) {
    Matcher matcher = ANCHORED_PARAMETER_PATTERN.matcher(s);
    if (matcher.find()) {
      @SuppressWarnings(
          "nullness:assignment") // group 1 is non-null due to the structure of the regex
      @NonNull String group1 = matcher.group(1);
      return Integer.parseInt(group1);
    }
    return -1;
  }

  // ///////////////////////////////////////////////////////////////////////////
  // Contexts
  //

  /**
   * Returns the type of the innermost enclosing class. Returns Type.noType if the type is a
   * top-level class.
   *
   * <p>If the innermost enclosing class is static, this method returns the type of that class. By
   * contrast, {@link DeclaredType#getEnclosingType()} returns the type of the innermost enclosing
   * class that is not static.
   *
   * @param type a DeclaredType
   * @return the type of the innermost enclosing class or Type.noType
   */
  private static TypeMirror getTypeOfEnclosingClass(DeclaredType type) {
    if (type instanceof ClassType) {
      // enclClass() needs to be called on tsym.owner, because tsym.enclClass() == tsym.
      Symbol sym = ((ClassType) type).tsym.owner;
      if (sym == null) {
        return com.sun.tools.javac.code.Type.noType;
      }

      ClassSymbol cs = sym.enclClass();
      if (cs == null) {
        return com.sun.tools.javac.code.Type.noType;
      }

      return cs.asType();
    } else {
      return type.getEnclosingType();
    }
  }

  // ///////////////////////////////////////////////////////////////////////////
  // Exceptions
  //

  /**
   * An exception that indicates a parse error. Call {@link #getDiagMessage} to obtain a {@link
   * DiagMessage} that can be used for error reporting.
   */
  public static class JavaExpressionParseException extends Exception {
    /** The serial version identifier. */
    private static final long serialVersionUID = 2L;

    /** The error message key. */
    private final @CompilerMessageKey String errorKey;

    /** The arguments to the error message key. */
    @SuppressWarnings("serial") // I do not intend to serialize JavaExpressionParseException objects
    public final Object[] args;

    /**
     * Create a new JavaExpressionParseException.
     *
     * @param errorKey the error message key
     * @param args the arguments to the error message key
     */
    public JavaExpressionParseException(@CompilerMessageKey String errorKey, Object... args) {
      this(null, errorKey, args);
    }

    /**
     * Create a new JavaExpressionParseException.
     *
     * @param cause cause
     * @param errorKey the error message key
     * @param args the arguments to the error message key
     */
    public JavaExpressionParseException(
        @Nullable Throwable cause, @CompilerMessageKey String errorKey, Object... args) {
      super(cause);
      this.errorKey = errorKey;
      this.args = args;
    }

    @Override
    public String getMessage() {
      return errorKey + " " + Arrays.toString(args);
    }

    /**
     * Return a DiagMessage that can be used for error reporting.
     *
     * @return a DiagMessage that can be used for error reporting
     */
    public DiagMessage getDiagMessage() {
      return new DiagMessage(Diagnostic.Kind.ERROR, errorKey, args);
    }

    public boolean isFlowParseError() {
      return errorKey.endsWith("flowexpr.parse.error");
    }
  }

  /**
   * Returns a {@link JavaExpressionParseException} with error key "flowexpr.parse.error" for the
   * expression {@code expr} with explanation {@code explanation}.
   *
   * @param expr the string that could not be parsed
   * @param explanation an explanation of the parse failure
   * @return a {@link JavaExpressionParseException} for the expression {@code expr} with explanation
   *     {@code explanation}.
   */
  private static JavaExpressionParseException constructJavaExpressionParseError(
      String expr, String explanation) {
    if (expr == null) {
      throw new BugInCF("Must have an expression.");
    }
    if (explanation == null) {
      throw new BugInCF("Must have an explanation.");
    }
    return new JavaExpressionParseException(
        (Throwable) null, "flowexpr.parse.error", "Invalid '" + expr + "' because " + explanation);
  }

  /**
   * The unchecked exception equivalent of checked exception {@link JavaExpressionParseException}.
   */
  private static class ParseRuntimeException extends RuntimeException {
    private static final long serialVersionUID = 2L;
    private final JavaExpressionParseException exception;

    private ParseRuntimeException(JavaExpressionParseException exception) {
      this.exception = exception;
    }

    private JavaExpressionParseException getCheckedException() {
      return exception;
    }
  }
}
