import * as charCodes from "charcodes";
import { tt, type TokenType } from "../tokenizer/types.ts";
import type {
  AssignmentPattern,
  TSParameterProperty,
  Decorator,
  Expression,
  Identifier,
  Node,
  Pattern,
  RestElement,
  SpreadElement,
  ObjectOrClassMember,
  ClassMember,
  ObjectMember,
  TSPropertySignature,
  TSMethodSignature,
  PrivateName,
  ObjectExpression,
  ObjectPattern,
  ArrayPattern,
  AssignmentProperty,
  Assignable,
  VoidPattern,
  ArgumentPlaceholder,
  TSTypeCastExpression,
  TypeCastExpression,
} from "../types.ts";
import type { Position } from "../util/location.ts";
import {
  isStrictBindOnlyReservedWord,
  isStrictBindReservedWord,
} from "../util/identifier.ts";
import { NodeUtils, type Undone } from "./node.ts";
import { BindingFlag } from "../util/scopeflags.ts";
import type { ExpressionErrors } from "./util.ts";
import { Errors, type LValAncestor } from "../parse-error.ts";
import type Parser from "./index.ts";
import { OptionFlags } from "../options.ts";

const unwrapParenthesizedExpression = (node: Node): Node => {
  return node.type === "ParenthesizedExpression"
    ? unwrapParenthesizedExpression(node.expression)
    : node;
};

export const enum ParseBindingListFlags {
  ALLOW_EMPTY = 1 << 0,
  IS_FUNCTION_PARAMS = 1 << 1,
  IS_CONSTRUCTOR_PARAMS = 1 << 2,
}

export default abstract class LValParser extends NodeUtils {
  // Forward-declaration: defined in expression.js
  abstract parseIdentifier(liberal?: boolean): Identifier;
  abstract parseMaybeAssign(
    refExpressionErrors?: ExpressionErrors | null,
    afterLeftParse?: Function,
  ): Expression;

  abstract parseMaybeAssignAllowIn(
    refExpressionErrors?: ExpressionErrors | null,
    afterLeftParse?: Function,
  ): Expression;

  abstract parseObjectLike<T extends ObjectPattern | ObjectExpression>(
    close: TokenType,
    isPattern: boolean,
    refExpressionErrors?: ExpressionErrors,
  ): T;
  abstract parseObjPropValue(
    prop: any,
    startLoc: Position | null,
    isGenerator: boolean,
    isAsync: boolean,
    isPattern: boolean,
    isAccessor: boolean,
    refExpressionErrors?: ExpressionErrors | null,
  ): void;
  abstract parsePropertyName(
    prop:
      | ObjectOrClassMember
      | ClassMember
      | TSPropertySignature
      | TSMethodSignature,
  ): void;
  abstract parsePrivateName(): PrivateName;
  // Forward-declaration: defined in statement.js
  abstract parseDecorator(): Decorator;

  /**
   * Convert existing expression atom to assignable pattern
   * if possible. Also checks invalid destructuring targets:
   *
   * - Parenthesized Destructuring patterns
   * - RestElement is not the last element
   * - Missing `=` in assignment pattern
   *
   * NOTE: There is a corresponding "isAssignable" method.
   * When this one is updated, please check if also that one needs to be updated.
   *
   * @param node The expression atom
   * @param isLHS Whether we are parsing a LeftHandSideExpression.
   *              If isLHS is `true`, the following cases are allowed: `[(a)] = [0]`, `[(a.b)] = [0]`
   *              If isLHS is `false`, we are in an arrow function parameters list.
   */
  toAssignable(node: Node, isLHS: boolean = false): asserts node is Assignable {
    let parenthesized = undefined;
    if (node.type === "ParenthesizedExpression" || node.extra?.parenthesized) {
      parenthesized = unwrapParenthesizedExpression(node);
      if (isLHS) {
        // an LHS can be reinterpreted to a binding pattern but not vice versa.
        // therefore a parenthesized identifier is ambiguous until we are sure it is an assignment expression
        // i.e. `([(a) = []] = []) => {}`
        // see also `recordArrowParameterBindingError` signature in packages/babel-parser/src/util/expression-scope.js
        if (parenthesized.type === "Identifier") {
          this.expressionScope.recordArrowParameterBindingError(
            Errors.InvalidParenthesizedAssignment,
            node,
          );
        } else if (
          parenthesized.type !== "CallExpression" &&
          parenthesized.type !== "MemberExpression" &&
          !this.isOptionalMemberExpression(parenthesized)
        ) {
          // A parenthesized member expression can be in LHS but not in pattern.
          // If the LHS is later interpreted as a pattern, `checkLVal` will throw for member expression binding
          // i.e. `([(a.b) = []] = []) => {}`
          this.raise(Errors.InvalidParenthesizedAssignment, node);
        }
      } else {
        this.raise(Errors.InvalidParenthesizedAssignment, node);
      }
    }

    switch (node.type) {
      case "Identifier":
      case "ObjectPattern":
      case "ArrayPattern":
      case "AssignmentPattern":
      case "RestElement":
      case "VoidPattern":
        break;

      case "ObjectExpression":
        this.castNodeTo(node, "ObjectPattern");
        for (
          let i = 0, length = node.properties.length, last = length - 1;
          i < length;
          i++
        ) {
          const prop = node.properties[i];
          const isLast = i === last;
          this.toAssignableObjectExpressionProp(prop, isLast, isLHS);

          if (
            isLast &&
            (prop as Node).type === "RestElement" &&
            node.extra?.trailingCommaLoc
          ) {
            this.raise(Errors.RestTrailingComma, node.extra.trailingCommaLoc);
          }
        }
        break;

      case "ObjectProperty": {
        const { key, value } = node;
        if (this.isPrivateName(key)) {
          this.classScope.usePrivateName(
            this.getPrivateNameSV(key),
            key.start!,
          );
        }
        this.toAssignable(value, isLHS);
        break;
      }

      case "SpreadElement": {
        throw new Error(
          "Internal @babel/parser error (this is a bug, please report it)." +
            " SpreadElement should be converted by .toAssignable's caller.",
        );
      }

      case "ArrayExpression":
        this.castNodeTo(node, "ArrayPattern");
        this.toAssignableList(
          node.elements,
          node.extra?.trailingCommaLoc as Position,
          isLHS,
        );
        break;

      case "AssignmentExpression":
        if (node.operator !== "=") {
          this.raise(
            Errors.MissingEqInAssignment,
            this.optionFlags & OptionFlags.Locations
              ? node.left.loc!.end
              : node.left,
          );
        }

        this.castNodeTo(node, "AssignmentPattern");
        // @ts-expect-error delete non-optional properties
        delete node.operator;
        // @ts-expect-error VoidPattern is not allowed in AssignmentExpression, here we throw a recoverable error
        if (node.left.type === "VoidPattern") {
          this.raise(Errors.VoidPatternInitializer, node.left);
        }
        this.toAssignable(node.left, isLHS);
        break;

      case "ParenthesizedExpression":
        /*::invariant (parenthesized !== undefined) */
        // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
        this.toAssignable(parenthesized!, isLHS);
        break;

      default:
      // We don't know how to deal with this node. It will
      // be reported by a later call to checkLVal
    }
  }

  toAssignableObjectExpressionProp(
    prop: Node,
    isLast: boolean,
    isLHS: boolean,
  ) {
    if (prop.type === "ObjectMethod") {
      this.raise(
        prop.kind === "get" || prop.kind === "set"
          ? Errors.PatternHasAccessor
          : Errors.PatternHasMethod,
        prop.key,
      );
    } else if (prop.type === "SpreadElement") {
      this.castNodeTo(prop, "RestElement");
      const arg = prop.argument;
      this.checkToRestConversion(arg, /* allowPattern */ false);
      this.toAssignable(arg, isLHS);

      if (!isLast) {
        this.raise(Errors.RestTrailingComma, prop);
      }
    } else {
      this.toAssignable(prop, isLHS);
    }
  }

  // Convert list of expression atoms to binding list.

  toAssignableList(
    exprList: (
      | Expression
      | SpreadElement
      | RestElement
      | VoidPattern
      | AssignmentPattern
      | ArgumentPlaceholder
      | TSTypeCastExpression
      | TypeCastExpression
      | null
    )[],
    trailingCommaLoc: Position | undefined | null,
    isLHS: boolean,
  ): void {
    const end = exprList.length - 1;

    for (let i = 0; i <= end; i++) {
      const elt = exprList[i];
      if (!elt) continue;

      this.toAssignableListItem(
        exprList as (
          | Expression
          | SpreadElement
          | RestElement
          | VoidPattern
          | AssignmentPattern
        )[],
        i,
        isLHS,
      );

      if (elt.type === "RestElement") {
        if (i < end) {
          this.raise(Errors.RestTrailingComma, elt);
        } else if (trailingCommaLoc) {
          this.raise(Errors.RestTrailingComma, trailingCommaLoc);
        }
      }
    }
  }

  toAssignableListItem(
    exprList: (
      | Expression
      | SpreadElement
      | RestElement
      | VoidPattern
      | AssignmentPattern
      | TSTypeCastExpression
    )[],
    index: number,
    isLHS: boolean,
  ): void {
    const node = exprList[index];
    if (node.type === "SpreadElement") {
      this.castNodeTo(node, "RestElement");
      const arg = node.argument;
      this.checkToRestConversion(arg, /* allowPattern */ true);
      this.toAssignable(arg, isLHS);
    } else {
      this.toAssignable(node, isLHS);
    }
  }

  isAssignable(node: Node, isBinding?: boolean): boolean {
    switch (node.type) {
      case "Identifier":
      case "ObjectPattern":
      case "ArrayPattern":
      case "AssignmentPattern":
      case "RestElement":
      case "VoidPattern":
        return true;

      case "ObjectExpression": {
        const last = node.properties.length - 1;
        return node.properties.every((prop, i) => {
          return (
            prop.type !== "ObjectMethod" &&
            (i === last || prop.type !== "SpreadElement") &&
            this.isAssignable(prop)
          );
        });
      }

      case "ObjectProperty":
        return this.isAssignable(node.value);

      case "SpreadElement":
        return this.isAssignable(node.argument);

      case "ArrayExpression":
        return node.elements.every(
          element => element === null || this.isAssignable(element),
        );

      case "AssignmentExpression":
        return node.operator === "=";

      case "ParenthesizedExpression":
        return this.isAssignable(node.expression);

      case "MemberExpression":
      case "OptionalMemberExpression":
        return !isBinding;

      default:
        return false;
    }
  }

  /**
   * Convert list of maybe-pattern atoms to a list of expression atoms.
   */
  toReferencedList(
    exprList:
      | readonly (
          | Expression
          | SpreadElement
          | VoidPattern
          | AssignmentPattern
          | ArgumentPlaceholder
          | TSTypeCastExpression
          | TypeCastExpression
          | null
        )[]
      | readonly (
          | Expression
          | RestElement
          | VoidPattern
          | AssignmentPattern
          | TSTypeCastExpression
          | TypeCastExpression
          | null
        )[],
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    isParenthesizedExpr?: boolean,
  ):
    | readonly (
        | Expression
        | SpreadElement
        | VoidPattern
        | AssignmentPattern
        | ArgumentPlaceholder
        | TSTypeCastExpression
        | TypeCastExpression
        | null
      )[]
    | readonly (
        | Expression
        | RestElement
        | VoidPattern
        | AssignmentPattern
        | TSTypeCastExpression
        | TypeCastExpression
        | null
      )[] {
    return exprList;
  }

  // Parses spread element.

  parseSpread(
    this: Parser,
    refExpressionErrors?: ExpressionErrors | null,
  ): SpreadElement {
    const node = this.startNode<SpreadElement>();
    this.next();
    node.argument = this.parseMaybeAssignAllowIn(
      refExpressionErrors,
      undefined,
    );
    return this.finishNode(node, "SpreadElement");
  }

  // https://tc39.es/ecma262/#prod-BindingRestElement
  parseRestBinding(this: Parser): RestElement {
    const node = this.startNode<RestElement>();
    this.next(); // eat `...`
    const argument = this.parseBindingAtom();
    if (argument.type === "VoidPattern") {
      this.raise(Errors.UnexpectedVoidPattern, argument);
    }
    // @ts-expect-error VoidPattern is not allowed in RestElement, here we parse it
    // and throw a recoverable error
    node.argument = argument;
    return this.finishNode(node, "RestElement");
  }

  // Parses lvalue (assignable) atom.
  parseBindingAtom(
    this: Parser,
  ): ArrayPattern | ObjectPattern | VoidPattern | Identifier {
    // https://tc39.es/ecma262/#prod-BindingPattern
    switch (this.state.type) {
      case tt.bracketL: {
        const node = this.startNode<ArrayPattern>();
        this.next();
        node.elements = this.parseBindingList(
          tt.bracketR,
          charCodes.rightSquareBracket,
          ParseBindingListFlags.ALLOW_EMPTY,
        );
        return this.finishNode(node, "ArrayPattern");
      }

      case tt.braceL:
        return this.parseObjectLike(tt.braceR, true);

      case tt._void:
        return this.parseVoidPattern(null);
    }

    // https://tc39.es/ecma262/#prod-BindingIdentifier
    return this.parseIdentifier();
  }

  // https://tc39.es/ecma262/#prod-BindingElementList
  parseBindingList(
    this: Parser,
    close: TokenType,
    closeCharCode: (typeof charCodes)[keyof typeof charCodes],
    flags: ParseBindingListFlags.ALLOW_EMPTY,
  ): (Pattern | RestElement | null)[];
  parseBindingList(
    this: Parser,
    close: TokenType,
    closeCharCode: (typeof charCodes)[keyof typeof charCodes],
    flags: ParseBindingListFlags.IS_FUNCTION_PARAMS,
  ): (Pattern | RestElement | TSParameterProperty)[];
  parseBindingList(
    this: Parser,
    close: TokenType,
    closeCharCode: (typeof charCodes)[keyof typeof charCodes],
    flags: ParseBindingListFlags,
  ): (Pattern | Identifier | RestElement | TSParameterProperty | null)[] {
    const allowEmpty = flags & ParseBindingListFlags.ALLOW_EMPTY;

    const elts: (
      | Pattern
      | Identifier
      | RestElement
      | TSParameterProperty
      | null
    )[] = [];
    let first = true;
    while (!this.eat(close)) {
      if (first) {
        first = false;
      } else {
        this.expect(tt.comma);
      }
      if (allowEmpty && this.match(tt.comma)) {
        elts.push(null);
      } else if (this.eat(close)) {
        break;
      } else if (this.match(tt.ellipsis)) {
        let rest = this.parseRestBinding();
        if (flags & ParseBindingListFlags.IS_FUNCTION_PARAMS) {
          rest = this.parseFunctionParamType(rest);
        }
        elts.push(rest);
        if (!this.checkCommaAfterRest(closeCharCode)) {
          this.expect(close);
          break;
        }
      } else {
        const decorators = [];
        if (flags & ParseBindingListFlags.IS_FUNCTION_PARAMS) {
          if (this.match(tt.at) && this.hasPlugin("decorators")) {
            this.raise(
              Errors.UnsupportedParameterDecorator,
              this.state.startLoc,
            );
          }
          // invariant: hasPlugin("decorators-legacy")
          while (this.match(tt.at)) {
            decorators.push(this.parseDecorator());
          }
        }
        elts.push(this.parseBindingElement(flags, decorators));
      }
    }
    return elts;
  }

  // https://tc39.es/ecma262/#prod-BindingRestProperty
  parseBindingRestProperty(
    this: Parser,
    prop: Undone<RestElement>,
  ): RestElement {
    this.next(); // eat '...'
    if (this.hasPlugin("discardBinding") && this.match(tt._void)) {
      // @ts-expect-error VoidPattern is not allowed in RestElement, here we parse it
      // and throw a recoverable error
      prop.argument = this.parseVoidPattern(null);
      this.raise(Errors.UnexpectedVoidPattern, prop.argument);
    } else {
      // Don't use parseRestBinding() as we only allow Identifier here.
      prop.argument = this.parseIdentifier();
    }
    this.checkCommaAfterRest(charCodes.rightCurlyBrace);
    return this.finishNode(prop, "RestElement");
  }

  // https://tc39.es/ecma262/#prod-BindingProperty
  parseBindingProperty(this: Parser): AssignmentProperty | RestElement {
    const { type, startLoc } = this.state;
    if (type === tt.ellipsis) {
      return this.parseBindingRestProperty(this.startNode());
    }

    const prop = this.startNode<AssignmentProperty>();
    if (type === tt.privateName) {
      this.expectPlugin("destructuringPrivate", startLoc);
      this.classScope.usePrivateName(this.state.value, startLoc);
      prop.key = this.parsePrivateName();
    } else {
      this.parsePropertyName(prop);
    }
    // @ts-expect-error todo: Undocumented AST property for ESTree compatibility
    // consider move it to the estree parser
    prop.method = false;
    return this.parseObjPropValue(
      prop,
      startLoc,
      false /* isGenerator */,
      false /* isAsync */,
      true /* isPattern */,
      false /* isAccessor */,
    );
  }

  // https://tc39.es/ecma262/#prod-BindingElement
  parseBindingElement(
    this: Parser,
    flags: ParseBindingListFlags,
    decorators: Decorator[],
  ): Pattern | Identifier | TSParameterProperty {
    const { startLoc } = this.state;
    const left = this.parseMaybeDefault();
    if (flags & ParseBindingListFlags.IS_FUNCTION_PARAMS) {
      this.parseFunctionParamType(left);
    }
    if (decorators.length) {
      // @ts-expect-error Todo: throw an error if decorators come before a void pattern
      left.decorators = decorators;
      this.resetStartLocationFromNode(left, decorators[0]);
    }
    const elt = this.parseMaybeDefault(startLoc, left);
    return elt;
  }

  // Used by flow/typescript plugin to add type annotations to binding elements
  parseFunctionParamType<T extends Pattern | Identifier | RestElement>(
    param: T,
  ): T {
    return param;
  }

  // Parses assignment pattern around given atom if possible.
  // https://tc39.es/ecma262/#prod-BindingElement
  parseMaybeDefault<P extends Pattern | Identifier>(
    this: Parser,
    startLoc?: Position | null,
    left?: P | null,
  ): P | AssignmentPattern;
  parseMaybeDefault(
    this: Parser,
    startLoc?: Position | null,
    left?: Pattern | Identifier | null,
  ): Pattern | Identifier {
    startLoc ??= this.state.startLoc;
    left = left ?? this.parseBindingAtom();
    if (!this.eat(tt.eq)) return left;

    const node = this.startNodeAt<AssignmentPattern>(startLoc);
    if (left.type === "VoidPattern") {
      this.raise(Errors.VoidPatternInitializer, left);
    }
    // @ts-expect-error VoidPattern is not allowed in AssignmentPattern, here we parse it
    // and throw a recoverable error
    node.left = left;
    node.right = this.parseMaybeAssignAllowIn();
    return this.finishNode(node, "AssignmentPattern");
  }
  /**
   * Return information use in determining whether a Node of a given type is an LVal,
   * possibly given certain additional context information.
   *
   * Subclasser notes: This method has kind of a lot of mixed, but related,
   * responsibilities. If we can definitively determine with the information
   * provided that this either *is* or *isn't* a valid `LVal`, then the return
   * value is easy: just return `true` or `false`. However, if it is a valid
   * LVal *ancestor*, and thus its descendants must be subsequently visited to
   * continue the "investigation", then this method should return the relevant
   * child key as a `string`. In some special cases, you additionally want to
   * convey that this node should be treated as if it were parenthesized. In
   * that case, a tuple of [key: string, parenthesized: boolean] is returned.
   * The `string`-only return option is actually just a shorthand for:
   * `[key: string, parenthesized: false]`.
   *
   * @param type A Node `type` string
   * @param disallowCallExpression Whether to disallow `CallExpression` as an LVal.
   * @param isUnparenthesizedInAssign
   *        Whether the node in question is unparenthesized and its parent
   *        is either an assignment pattern or an assignment expression.
   * @param binding
   *        The binding operation that is being considered for this potential
   *        LVal.
   * @returns `true` or `false` if we can immediately determine whether the node
   *          type in question can be treated as an `LVal`.
   *          A `string` key to traverse if we must check this child.
   *          A `[string, boolean]` tuple if we need to check this child and
   *          treat is as parenthesized.
   */
  isValidLVal(
    type: string,
    disallowCallExpression: boolean,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    isUnparenthesizedInAssign: boolean,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    binding: BindingFlag,
  ): string | boolean | [string, boolean] {
    switch (type) {
      case "AssignmentPattern":
        return "left";
      case "RestElement":
        return "argument";
      case "ObjectProperty":
        return "value";
      case "ParenthesizedExpression":
        return "expression";
      case "ArrayPattern":
        return "elements";
      case "ObjectPattern":
        return "properties";
      case "VoidPattern":
        return true;
      case "CallExpression":
        if (
          !disallowCallExpression &&
          !this.state.strict &&
          this.optionFlags & OptionFlags.AnnexB
        ) {
          return true;
        }
    }
    return false;
  }

  // Overridden by the estree plugin
  isOptionalMemberExpression(expression: Node): boolean {
    return expression.type === "OptionalMemberExpression";
  }

  /**
   * Verify that a target expression is an lval (something that can be assigned to).
   *
   * @param expression The expression in question to check.
   * @param ancestor
   *        The relevant ancestor to provide context information for the error
   *        if the check fails.
   * @param binding
   *        The desired binding type. If the given expression is an identifier
   *        and `binding` is not `BindingFlag.TYPE_NONE`, `checkLVal` will register binding
   *        to the parser scope See also `src/util/scopeflags.js`
   * @param checkClashes
   *        An optional string set to check if an identifier name is included.
   *        `checkLVal` will add checked identifier name to `checkClashes` It is
   *        used in tracking duplicates in function parameter lists. If it is
   *        false, `checkLVal` will skip duplicate checks
   * @param strictModeChanged
   *        Whether an identifier has been parsed in a sloppy context but should
   *        be reinterpreted as strict-mode. e.g. `(arguments) => { "use strict "}`
   * @param hasParenthesizedAncestor
   *        This is only used internally during recursive calls, and you should
   *        not have to set it yourself.
   * @param disallowCallExpression Whether to disallow `CallExpression` as an LVal.
   */

  checkLVal(
    expression:
      | Expression
      | ObjectMember
      | RestElement
      | Pattern
      | TSParameterProperty,
    ancestor: LValAncestor,
    binding: BindingFlag = BindingFlag.TYPE_NONE,
    checkClashes: Set<string> | false = false,
    strictModeChanged: boolean = false,
    hasParenthesizedAncestor: boolean = false,
    disallowCallExpression: boolean = false,
  ): void {
    const type = expression.type;

    // If we find here an ObjectMethod, it's because this was originally
    // an ObjectExpression which has then been converted.
    // toAssignable already reported this error with a nicer message.
    if (this.isObjectMethod(expression)) return;

    const isOptionalMemberExpression =
      this.isOptionalMemberExpression(expression);

    if (isOptionalMemberExpression || type === "MemberExpression") {
      if (isOptionalMemberExpression) {
        this.expectPlugin("optionalChainingAssign", expression.start);
        if (ancestor.type !== "AssignmentExpression") {
          this.raise(Errors.InvalidLhsOptionalChaining, expression, {
            ancestor,
          });
        }
      }

      if (binding !== BindingFlag.TYPE_NONE) {
        this.raise(Errors.InvalidPropertyBindingPattern, expression);
      }
      return;
    }

    if (type === "Identifier") {
      this.checkIdentifier(expression, binding, strictModeChanged);

      const { name } = expression;

      if (checkClashes) {
        if (checkClashes.has(name)) {
          this.raise(Errors.ParamDupe, expression);
        } else {
          checkClashes.add(name);
        }
      }

      return;
    } else if (type === "VoidPattern" && ancestor.type === "CatchClause") {
      this.raise(Errors.VoidPatternCatchClauseParam, expression);
    }

    const unwrappedExpression = unwrapParenthesizedExpression(expression);
    disallowCallExpression ||=
      unwrappedExpression.type === "CallExpression" &&
      (unwrappedExpression.callee.type === "Import" ||
        unwrappedExpression.callee.type === "Super");
    const validity = this.isValidLVal(
      type,
      disallowCallExpression,
      !(hasParenthesizedAncestor || expression.extra?.parenthesized) &&
        ancestor.type === "AssignmentExpression",
      binding,
    );

    if (validity === true) return;
    if (validity === false) {
      const ParseErrorClass =
        binding === BindingFlag.TYPE_NONE
          ? Errors.InvalidLhs
          : Errors.InvalidLhsBinding;

      this.raise(ParseErrorClass, expression, { ancestor });
      return;
    }

    let key: string, isParenthesizedExpression: boolean;
    if (typeof validity === "string") {
      key = validity;
      isParenthesizedExpression = type === "ParenthesizedExpression";
    } else {
      [key, isParenthesizedExpression] = validity;
    }

    const nextAncestor =
      type === "ArrayPattern" || type === "ObjectPattern"
        ? ({ type } as const)
        : ancestor;

    // @ts-expect-error key may not index expression.
    const val = expression[key];
    if (Array.isArray(val)) {
      for (const child of val) {
        if (child) {
          this.checkLVal(
            child,
            nextAncestor,
            binding,
            checkClashes,
            strictModeChanged,
            isParenthesizedExpression,
            true,
          );
        }
      }
    } else if (val) {
      this.checkLVal(
        val,
        nextAncestor,
        binding,
        checkClashes,
        strictModeChanged,
        isParenthesizedExpression,
        disallowCallExpression,
      );
    }
  }

  checkIdentifier(
    at: Identifier,
    bindingType: BindingFlag,
    strictModeChanged: boolean = false,
  ) {
    if (
      this.state.strict &&
      (strictModeChanged
        ? isStrictBindReservedWord(at.name, this.inModule)
        : isStrictBindOnlyReservedWord(at.name))
    ) {
      if (bindingType === BindingFlag.TYPE_NONE) {
        this.raise(Errors.StrictEvalArguments, at, { referenceName: at.name });
      } else {
        this.raise(Errors.StrictEvalArgumentsBinding, at, {
          bindingName: at.name,
        });
      }
    }

    if (bindingType & BindingFlag.FLAG_NO_LET_IN_LEXICAL && at.name === "let") {
      this.raise(Errors.LetInLexicalBinding, at);
    }

    if (!(bindingType & BindingFlag.TYPE_NONE)) {
      this.declareNameFromIdentifier(at, bindingType);
    }
  }

  declareNameFromIdentifier(identifier: Identifier, binding: BindingFlag) {
    this.scope.declareName(identifier.name, binding, identifier.start!);
  }

  checkToRestConversion(node: Node, allowPattern: boolean): void {
    switch (node.type) {
      case "ParenthesizedExpression":
        this.checkToRestConversion(node.expression, allowPattern);
        break;
      case "Identifier":
      case "MemberExpression":
        break;
      case "ArrayExpression":
      case "ObjectExpression":
        if (allowPattern) break;
      /* falls through */
      default:
        this.raise(Errors.InvalidRestAssignmentPattern, node);
    }
  }

  checkCommaAfterRest(
    close: (typeof charCodes)[keyof typeof charCodes],
  ): boolean {
    if (!this.match(tt.comma)) {
      return false;
    }

    this.raise(
      this.lookaheadCharCode() === close
        ? Errors.RestTrailingComma
        : Errors.ElementAfterRest,
      this.state.startLoc,
    );

    return true;
  }
}
