/*:: declare var invariant; */

import type Parser from "../../parser/index.ts";
import {
  tokenIsIdentifier,
  tokenIsKeyword,
  tokenIsKeywordOrIdentifier,
  tokenIsLiteralPropertyName,
  tokenLabelName,
  tt,
  type TokenType,
  tokenIsFlowInterfaceOrTypeOrOpaque,
} from "../../tokenizer/types.ts";
import type * as N from "../../types.ts";
import type { Position } from "../../util/location.ts";
import { types as tc } from "../../tokenizer/context.ts";
import * as charCodes from "charcodes";
import { isIteratorStart } from "../../util/identifier.ts";
import FlowScopeHandler from "./scope.ts";
import { BindingFlag, ScopeFlag } from "../../util/scopeflags.ts";
import type { ExpressionErrors } from "../../parser/util.ts";
import type { ParseStatementFlag } from "../../parser/statement.ts";
import {
  Errors,
  ParseErrorEnum,
  type ParseErrorTemplates,
} from "../../parse-error.ts";
import type { Undone } from "../../parser/node.ts";
import type { ClassWithMixin, IJSXParserMixin } from "../jsx/index.ts";

const reservedTypes = new Set([
  "_",
  "any",
  "bool",
  "boolean",
  "empty",
  "extends",
  "false",
  "interface",
  "mixed",
  "null",
  "number",
  "static",
  "string",
  "true",
  "typeof",
  "void",
]);

/* eslint sort-keys: "error" */
// The Errors key follows https://github.com/facebook/flow/blob/master/src/parser/parse_error.ml unless it does not exist
export const FlowErrorTemplates = {
  AmbiguousConditionalArrow:
    "Ambiguous expression: wrap the arrow functions in parentheses to disambiguate.",
  AmbiguousDeclareModuleKind:
    "Found both `declare module.exports` and `declare export` in the same module. Modules can only have 1 since they are either an ES module or they are a CommonJS module.",
  // TODO: When we get proper string enums in typescript make this ReservedType.
  // Not really worth it to do the whole $Values dance with reservedTypes set.
  AssignReservedType: ({ reservedType }: { reservedType: string }) =>
    `Cannot overwrite reserved type ${reservedType}.`,
  DeclareClassElement:
    "The `declare` modifier can only appear on class fields.",
  DeclareClassFieldInitializer:
    "Initializers are not allowed in fields with the `declare` modifier.",
  DuplicateDeclareModuleExports:
    "Duplicate `declare module.exports` statement.",
  EnumBooleanMemberNotInitialized: ({
    memberName,
    enumName,
  }: {
    memberName: string;
    enumName: string;
  }) =>
    `Boolean enum members need to be initialized. Use either \`${memberName} = true,\` or \`${memberName} = false,\` in enum \`${enumName}\`.`,
  EnumDuplicateMemberName: ({
    memberName,
    enumName,
  }: {
    memberName: string;
    enumName: string;
  }) =>
    `Enum member names need to be unique, but the name \`${memberName}\` has already been used before in enum \`${enumName}\`.`,
  EnumInconsistentMemberValues: ({ enumName }: { enumName: string }) =>
    `Enum \`${enumName}\` has inconsistent member initializers. Either use no initializers, or consistently use literals (either booleans, numbers, or strings) for all member initializers.`,
  EnumInvalidExplicitType: ({
    invalidEnumType,
    enumName,
  }: {
    invalidEnumType: string;
    enumName: string;
  }) =>
    `Enum type \`${invalidEnumType}\` is not valid. Use one of \`boolean\`, \`number\`, \`string\`, or \`symbol\` in enum \`${enumName}\`.`,
  EnumInvalidExplicitTypeUnknownSupplied: ({
    enumName,
  }: {
    enumName: string;
  }) =>
    `Supplied enum type is not valid. Use one of \`boolean\`, \`number\`, \`string\`, or \`symbol\` in enum \`${enumName}\`.`,

  // TODO: When moving to typescript, we should either have each of the
  // following errors only accept the specific strings they want:
  //
  // ...PrimaryType: explicitType: "string" | "number" | "boolean"
  // ...SymbolType: explicitType: "symbol"
  // ...UnknownType: explicitType: null
  //
  // Or, alternatively, merge these three errors together into one
  // `EnumInvalidMemberInitializer` error that can accept `EnumExplicitType`
  // without alteration, and then just have its message change based on the
  // explicitType.
  EnumInvalidMemberInitializerPrimaryType: ({
    enumName,
    memberName,
    explicitType,
  }: {
    enumName: string;
    memberName: string;
    explicitType: EnumExplicitType;
  }) =>
    `Enum \`${enumName}\` has type \`${explicitType}\`, so the initializer of \`${memberName}\` needs to be a ${explicitType} literal.`,
  EnumInvalidMemberInitializerSymbolType: ({
    enumName,
    memberName,
  }: {
    enumName: string;
    memberName: string;
    explicitType: EnumExplicitType;
  }) =>
    `Symbol enum members cannot be initialized. Use \`${memberName},\` in enum \`${enumName}\`.`,
  EnumInvalidMemberInitializerUnknownType: ({
    enumName,
    memberName,
  }: {
    enumName: string;
    memberName: string;
    explicitType: EnumExplicitType;
  }) =>
    `The enum member initializer for \`${memberName}\` needs to be a literal (either a boolean, number, or string) in enum \`${enumName}\`.`,
  EnumInvalidMemberName: ({
    enumName,
    memberName,
    suggestion,
  }: {
    enumName: string;
    memberName: string;
    suggestion: string;
  }) =>
    `Enum member names cannot start with lowercase 'a' through 'z'. Instead of using \`${memberName}\`, consider using \`${suggestion}\`, in enum \`${enumName}\`.`,
  EnumNumberMemberNotInitialized: ({
    enumName,
    memberName,
  }: {
    enumName: string;
    memberName: string;
  }) =>
    `Number enum members need to be initialized, e.g. \`${memberName} = 1\` in enum \`${enumName}\`.`,
  EnumStringMemberInconsistentlyInitialized: ({
    enumName,
  }: {
    enumName: string;
  }) =>
    `String enum members need to consistently either all use initializers, or use no initializers, in enum \`${enumName}\`.`,
  GetterMayNotHaveThisParam: "A getter cannot have a `this` parameter.",
  ImportTypeShorthandOnlyInPureImport:
    "The `type` and `typeof` keywords on named imports can only be used on regular `import` statements. It cannot be used with `import type` or `import typeof` statements.",
  InexactInsideExact:
    "Explicit inexact syntax cannot appear inside an explicit exact object type.",
  InexactInsideNonObject:
    "Explicit inexact syntax cannot appear in class or interface definitions.",
  InexactVariance: "Explicit inexact syntax cannot have variance.",
  InvalidNonTypeImportInDeclareModule:
    "Imports within a `declare module` body must always be `import type` or `import typeof`.",
  MissingTypeParamDefault:
    "Type parameter declaration needs a default, since a preceding type parameter declaration has a default.",
  NestedDeclareModule:
    "`declare module` cannot be used inside another `declare module`.",
  NestedFlowComment: "Cannot have a flow comment inside another flow comment.",
  PatternIsOptional: {
    message:
      "A binding pattern parameter cannot be optional in an implementation signature.",
  },

  SetterMayNotHaveThisParam: "A setter cannot have a `this` parameter.",
  SpreadVariance: "Spread properties cannot have variance.",
  ThisParamAnnotationRequired:
    "A type annotation is required for the `this` parameter.",
  ThisParamBannedInConstructor:
    "Constructors cannot have a `this` parameter; constructors don't bind `this` like other functions.",
  ThisParamMayNotBeOptional: "The `this` parameter cannot be optional.",
  ThisParamMustBeFirst:
    "The `this` parameter must be the first function parameter.",
  ThisParamNoDefault: "The `this` parameter may not have a default value.",
  TypeBeforeInitializer:
    "Type annotations must come before default assignments, e.g. instead of `age = 25: number` use `age: number = 25`.",
  TypeCastInPattern:
    "The type cast expression is expected to be wrapped with parenthesis.",
  UnexpectedExplicitInexactInObject:
    "Explicit inexact syntax must appear at the end of an inexact object.",
  UnexpectedReservedType: ({ reservedType }: { reservedType: string }) =>
    `Unexpected reserved type ${reservedType}.`,
  UnexpectedReservedUnderscore:
    "`_` is only allowed as a type argument to call or new.",
  UnexpectedSpaceBetweenModuloChecks:
    "Spaces between `%` and `checks` are not allowed here.",
  UnexpectedSpreadType:
    "Spread operator cannot appear in class or interface definitions.",
  UnexpectedSubtractionOperand:
    'Unexpected token, expected "number" or "bigint".',
  UnexpectedTokenAfterTypeParameter:
    "Expected an arrow function after this type parameter declaration.",
  UnexpectedTypeParameterBeforeAsyncArrowFunction:
    "Type parameters must come after the async keyword, e.g. instead of `<T> async () => {}`, use `async <T>() => {}`.",
  UnsupportedDeclareExportKind: ({
    unsupportedExportKind,
    suggestion,
  }: {
    unsupportedExportKind: string;
    suggestion: string;
  }) =>
    `\`declare export ${unsupportedExportKind}\` is not supported. Use \`${suggestion}\` instead.`,
  UnsupportedStatementInDeclareModule:
    "Only declares and type imports are allowed inside declare module.",
  UnterminatedFlowComment: "Unterminated flow-comment.",
} satisfies ParseErrorTemplates;
/* eslint-disable sort-keys */

const FlowErrors = ParseErrorEnum`flow`(FlowErrorTemplates);

function isEsModuleType(bodyElement: N.Node): boolean {
  return (
    bodyElement.type === "DeclareExportAllDeclaration" ||
    (bodyElement.type === "DeclareExportDeclaration" &&
      (!bodyElement.declaration ||
        (bodyElement.declaration.type !== "TypeAlias" &&
          bodyElement.declaration.type !== "InterfaceDeclaration")))
  );
}

function hasTypeImportKind(
  node: Undone<N.ImportSpecifier | N.ImportDeclaration>,
): boolean {
  return node.importKind === "type" || node.importKind === "typeof";
}

const exportSuggestions = {
  const: "declare export var",
  let: "declare export var",
  type: "export type",
  interface: "export interface",
};

// Like Array#filter, but returns a tuple [ acceptedElements, discardedElements ]
function partition<T>(
  list: T[],
  test: (c: T, b: number, a: T[]) => boolean | undefined | null,
): [T[], T[]] {
  const list1: T[] = [];
  const list2: T[] = [];
  for (let i = 0; i < list.length; i++) {
    (test(list[i], i, list) ? list1 : list2).push(list[i]);
  }
  return [list1, list2];
}

const FLOW_PRAGMA_REGEX = /\*?\s*@((?:no)?flow)\b/;

// Flow enums types
type EnumExplicitType = null | "boolean" | "number" | "string" | "symbol";

type EnumContext = {
  enumName: string;
  explicitType: EnumExplicitType;
  memberName: string;
};

type EnumMemberInit =
  | {
      type: "number";
      loc: number;
      value: N.NumericLiteral;
    }
  | {
      type: "string";
      loc: number;
      value: N.StringLiteral;
    }
  | {
      type: "boolean";
      loc: number;
      value: N.BooleanLiteral;
    }
  | {
      type: "invalid";
      loc: Position;
    }
  | {
      type: "none";
      loc: Position;
    };

export default (superClass: ClassWithMixin<typeof Parser, IJSXParserMixin>) =>
  class FlowParserMixin extends superClass implements Parser {
    // The value of the @flow/@noflow pragma. Initially undefined, transitions
    // to "@flow" or "@noflow" if we see a pragma. Transitions to null if we are
    // past the initial comment.
    flowPragma: void | null | "flow" | "noflow" = undefined;

    getScopeHandler(): new (...args: any) => FlowScopeHandler {
      return FlowScopeHandler;
    }

    shouldParseTypes(): boolean {
      return this.getPluginOption("flow", "all") || this.flowPragma === "flow";
    }

    finishToken(type: TokenType, val: any): void {
      if (
        type !== tt.string &&
        type !== tt.semi &&
        type !== tt.interpreterDirective
      ) {
        if (this.flowPragma === undefined) {
          this.flowPragma = null;
        }
      }
      super.finishToken(type, val);
    }

    addComment(comment: N.Comment): void {
      if (this.flowPragma === undefined) {
        // Try to parse a flow pragma.
        const matches = FLOW_PRAGMA_REGEX.exec(comment.value);
        if (!matches) {
          // do nothing
        } else if (matches[1] === "flow") {
          this.flowPragma = "flow";
        } else if (matches[1] === "noflow") {
          this.flowPragma = "noflow";
        } else {
          throw new Error("Unexpected flow pragma");
        }
      }
      super.addComment(comment);
    }

    flowParseTypeInitialiser(tok?: TokenType): N.FlowType {
      const oldInType = this.state.inType;
      this.state.inType = true;
      this.expect(tok || tt.colon);

      const type = this.flowParseType();
      this.state.inType = oldInType;
      return type;
    }

    flowParsePredicate(): N.FlowPredicate {
      const node = this.startNode<N.FlowPredicate>();
      const moduloLoc = this.state.startLoc;
      this.next(); // eat `%`
      this.expectContextual(tt._checks);
      // Force '%' and 'checks' to be adjacent
      if (this.state.lastTokStartLoc!.index > moduloLoc.index + 1) {
        this.raise(FlowErrors.UnexpectedSpaceBetweenModuloChecks, moduloLoc);
      }
      if (this.eat(tt.parenL)) {
        (node as Undone<N.DeclaredPredicate>).value = super.parseExpression();
        this.expect(tt.parenR);
        return this.finishNode(node, "DeclaredPredicate");
      } else {
        return this.finishNode(node, "InferredPredicate");
      }
    }

    flowParseTypeAndPredicateInitialiser(
      allowLonePredicate: true,
    ): [N.FlowType | null, N.FlowPredicate | null];
    flowParseTypeAndPredicateInitialiser(
      allowLonePredicate: false,
    ): [N.FlowType, N.FlowPredicate | null];
    flowParseTypeAndPredicateInitialiser(
      allowLonePredicate: boolean,
    ): [N.FlowType | null, N.FlowPredicate | null] {
      const oldInType = this.state.inType;
      this.state.inType = true;
      this.expect(tt.colon);
      let type = null;
      let predicate = null;
      if (allowLonePredicate && this.match(tt.modulo)) {
        this.state.inType = oldInType;
        predicate = this.flowParsePredicate();
      } else {
        type = this.flowParseType();
        this.state.inType = oldInType;
        if (this.match(tt.modulo)) {
          predicate = this.flowParsePredicate();
        }
      }
      return [type, predicate];
    }

    flowParseDeclareClass(node: Undone<N.DeclareClass>): N.DeclareClass {
      this.next();
      this.flowParseInterfaceish(node, /*isClass*/ true);
      return this.finishNode(node, "DeclareClass");
    }

    flowParseDeclareFunction(
      node: Undone<N.DeclareFunction>,
    ): N.DeclareFunction {
      this.next();

      const id = (node.id = this.parseIdentifier());

      const typeNode = this.startNode<N.FunctionTypeAnnotation>();
      const typeContainer = this.startNode<N.TypeAnnotation>();

      if (this.match(tt.lt)) {
        typeNode.typeParameters = this.flowParseTypeParameterDeclaration();
      } else {
        typeNode.typeParameters = null;
      }

      this.expect(tt.parenL);
      const tmp = this.flowParseFunctionTypeParams();
      typeNode.params = tmp.params;
      typeNode.rest = tmp.rest;
      typeNode.this = tmp._this;
      this.expect(tt.parenR);

      [typeNode.returnType, node.predicate] =
        this.flowParseTypeAndPredicateInitialiser(false);

      typeContainer.typeAnnotation = this.finishNode(
        typeNode,
        "FunctionTypeAnnotation",
      );

      id.typeAnnotation = this.finishNode(typeContainer, "TypeAnnotation");

      this.resetEndLocation(id);
      this.semicolon();

      this.scope.declareName(
        node.id.name,
        BindingFlag.TYPE_FLOW_DECLARE_FN,
        node.id.start!,
      );

      return this.finishNode(node, "DeclareFunction");
    }

    flowParseDeclare(
      node: Undone<N.FlowDeclare>,
      insideModule?: boolean,
    ): N.FlowDeclare {
      if (this.match(tt._class)) {
        return this.flowParseDeclareClass(node as Undone<N.DeclareClass>);
      } else if (this.match(tt._function)) {
        return this.flowParseDeclareFunction(node as Undone<N.DeclareFunction>);
      } else if (this.match(tt._var)) {
        return this.flowParseDeclareVariable(node as Undone<N.DeclareVariable>);
      } else if (this.eatContextual(tt._module)) {
        if (this.match(tt.dot)) {
          return this.flowParseDeclareModuleExports(
            node as Undone<N.DeclareModuleExports>,
          );
        } else {
          if (insideModule) {
            this.raise(
              FlowErrors.NestedDeclareModule,
              this.state.lastTokStartLoc!,
            );
          }
          return this.flowParseDeclareModule(node as Undone<N.DeclareModule>);
        }
      } else if (this.isContextual(tt._type)) {
        return this.flowParseDeclareTypeAlias(
          node as Undone<N.DeclareTypeAlias>,
        );
      } else if (this.isContextual(tt._opaque)) {
        return this.flowParseDeclareOpaqueType(
          node as Undone<N.DeclareOpaqueType>,
        );
      } else if (this.isContextual(tt._interface)) {
        return this.flowParseDeclareInterface(
          node as Undone<N.DeclareInterface>,
        );
      } else if (this.match(tt._export)) {
        return this.flowParseDeclareExportDeclaration(node, insideModule);
      }
      throw this.unexpected();
    }

    flowParseDeclareVariable(
      node: Undone<N.DeclareVariable>,
    ): N.DeclareVariable {
      this.next();
      node.id = this.flowParseTypeAnnotatableIdentifier();
      this.scope.declareName(
        node.id.name,
        BindingFlag.TYPE_VAR,
        node.id.start!,
      );
      this.semicolon();
      return this.finishNode(node, "DeclareVariable");
    }

    flowParseDeclareModule(node: Undone<N.DeclareModule>): N.DeclareModule {
      this.scope.enter(ScopeFlag.OTHER);

      if (this.match(tt.string)) {
        node.id = super.parseExprAtom() as N.StringLiteral;
      } else {
        node.id = this.parseIdentifier();
      }

      const bodyNode = this.startNode<N.BlockStatement>();
      const body: N.Statement[] = (bodyNode.body = []);
      this.expect(tt.braceL);
      while (!this.match(tt.braceR)) {
        const bodyNode = this.startNode<N.ImportDeclaration>();

        if (this.match(tt._import)) {
          this.next();
          if (!this.isContextual(tt._type) && !this.match(tt._typeof)) {
            this.raise(
              FlowErrors.InvalidNonTypeImportInDeclareModule,
              this.state.lastTokStartLoc!,
            );
          }
          body.push(super.parseImport(bodyNode));
        } else {
          this.expectContextual(
            tt._declare,
            FlowErrors.UnsupportedStatementInDeclareModule,
          );
          body.push(this.flowParseDeclare(bodyNode, true));
        }
      }

      this.scope.exit();

      this.expect(tt.braceR);

      node.body = this.finishNode(bodyNode, "BlockStatement");

      let kind: "CommonJS" | "ES" | null = null;
      let hasModuleExport = false;
      body.forEach(bodyElement => {
        if (isEsModuleType(bodyElement)) {
          if (kind === "CommonJS") {
            this.raise(FlowErrors.AmbiguousDeclareModuleKind, bodyElement);
          }
          kind = "ES";
        } else if (bodyElement.type === "DeclareModuleExports") {
          if (hasModuleExport) {
            this.raise(FlowErrors.DuplicateDeclareModuleExports, bodyElement);
          }
          if (kind === "ES") {
            this.raise(FlowErrors.AmbiguousDeclareModuleKind, bodyElement);
          }
          kind = "CommonJS";
          hasModuleExport = true;
        }
      });

      node.kind = kind || "CommonJS";
      return this.finishNode(node, "DeclareModule");
    }

    flowParseDeclareExportDeclaration(
      node: Undone<N.DeclareExportDeclaration>,
      insideModule?: boolean | null,
    ): N.DeclareExportDeclaration | N.DeclareExportAllDeclaration {
      this.expect(tt._export);

      if (this.eat(tt._default)) {
        if (this.match(tt._function) || this.match(tt._class)) {
          // declare export default class ...
          // declare export default function ...
          node.declaration = this.flowParseDeclare(this.startNode());
        } else {
          // declare export default [type];
          node.declaration = this.flowParseType();
          this.semicolon();
        }
        node.default = true;

        return this.finishNode(node, "DeclareExportDeclaration");
      } else {
        if (
          this.match(tt._const) ||
          this.isLet() ||
          ((this.isContextual(tt._type) || this.isContextual(tt._interface)) &&
            !insideModule)
        ) {
          const label = this.state.value as
            | "const"
            | "let"
            | "type"
            | "interface";
          throw this.raise(
            FlowErrors.UnsupportedDeclareExportKind,
            this.state.startLoc,
            {
              unsupportedExportKind: label,
              suggestion: exportSuggestions[label],
            },
          );
        }

        if (
          this.match(tt._var) || // declare export var ...
          this.match(tt._function) || // declare export function ...
          this.match(tt._class) || // declare export class ...
          this.isContextual(tt._opaque) // declare export opaque ..
        ) {
          node.declaration = this.flowParseDeclare(this.startNode());
          node.default = false;

          return this.finishNode(node, "DeclareExportDeclaration");
        } else if (
          this.match(tt.star) || // declare export * from ''
          this.match(tt.braceL) || // declare export {} ...
          this.isContextual(tt._interface) || // declare export interface ...
          this.isContextual(tt._type) || // declare export type ...
          this.isContextual(tt._opaque) // declare export opaque type ...
        ) {
          const result = this.parseExport(node, /* decorators */ null);
          if (result.type === "ExportNamedDeclaration") {
            (result as unknown as N.DeclareExportDeclaration).default = false;
            delete result.exportKind;
            return this.castNodeTo(result, "DeclareExportDeclaration");
          } else {
            return this.castNodeTo(result, "DeclareExportAllDeclaration");
          }
        }
      }

      throw this.unexpected();
    }

    flowParseDeclareModuleExports(
      node: Undone<N.DeclareModuleExports>,
    ): N.DeclareModuleExports {
      this.next();
      this.expectContextual(tt._exports);
      node.typeAnnotation = this.flowParseTypeAnnotation();
      this.semicolon();

      return this.finishNode(node, "DeclareModuleExports");
    }

    flowParseDeclareTypeAlias(
      node: Undone<N.DeclareTypeAlias>,
    ): N.DeclareTypeAlias {
      this.next();
      const finished = this.flowParseTypeAlias(
        node,
      ) as unknown as N.DeclareTypeAlias;
      // Don't do finishNode as we don't want to process comments twice
      this.castNodeTo(finished, "DeclareTypeAlias");
      return finished;
    }

    flowParseDeclareOpaqueType(
      node: Undone<N.DeclareOpaqueType>,
    ): N.DeclareOpaqueType {
      this.next();
      return this.flowParseOpaqueType(node, true);
    }

    flowParseDeclareInterface(
      node: Undone<N.DeclareInterface>,
    ): N.DeclareInterface {
      this.next();
      this.flowParseInterfaceish(node, /* isClass */ false);
      return this.finishNode(node, "DeclareInterface");
    }

    // Interfaces

    flowParseInterfaceish(
      node: Undone<
        N.DeclareClass | N.DeclareInterface | N.InterfaceDeclaration
      >,
      isClass: boolean,
    ): void {
      node.id = this.flowParseRestrictedIdentifier(
        /* liberal */ !isClass,
        /* declaration */ true,
      );

      this.scope.declareName(
        node.id.name,
        isClass ? BindingFlag.TYPE_FUNCTION : BindingFlag.TYPE_LEXICAL,
        node.id.start!,
      );

      if (this.match(tt.lt)) {
        node.typeParameters = this.flowParseTypeParameterDeclaration();
      } else {
        node.typeParameters = null;
      }

      node.extends = [];

      if (this.eat(tt._extends)) {
        do {
          node.extends.push(this.flowParseInterfaceExtends());
        } while (!isClass && this.eat(tt.comma));
      }

      if (isClass) {
        const implemented: N.ClassImplements[] = [];
        const mixins: N.InterfaceExtends[] = [];

        if (this.eatContextual(tt._mixins)) {
          do {
            mixins.push(this.flowParseInterfaceExtends());
          } while (this.eat(tt.comma));
        }

        if (this.eatContextual(tt._implements)) {
          do {
            implemented.push(this.flowParseClassImplements());
          } while (this.eat(tt.comma));
        }
        (node as Undone<N.DeclareClass>).implements = implemented;
        (node as Undone<N.DeclareClass>).mixins = mixins;
      }

      node.body = this.flowParseObjectType({
        allowStatic: isClass,
        allowExact: false,
        allowSpread: false,
        allowProto: isClass,
        allowInexact: false,
      });
    }

    flowParseInterfaceExtends(): N.InterfaceExtends {
      const node = this.startNode<N.InterfaceExtends>();

      node.id = this.flowParseQualifiedTypeIdentifier();
      if (this.match(tt.lt)) {
        node.typeParameters = this.flowParseTypeParameterInstantiation();
      } else {
        node.typeParameters = null;
      }

      return this.finishNode(node, "InterfaceExtends");
    }

    flowParseInterface(
      node: Undone<N.InterfaceDeclaration>,
    ): N.InterfaceDeclaration {
      this.flowParseInterfaceish(node, /* isClass */ false);
      return this.finishNode(node, "InterfaceDeclaration");
    }

    checkNotUnderscore(word: string) {
      if (word === "_") {
        this.raise(
          FlowErrors.UnexpectedReservedUnderscore,
          this.state.startLoc,
        );
      }
    }

    checkReservedType(
      word: string,
      startLoc: Position | number,
      declaration?: boolean,
    ) {
      if (!reservedTypes.has(word)) return;

      this.raise(
        declaration
          ? FlowErrors.AssignReservedType
          : FlowErrors.UnexpectedReservedType,
        startLoc,
        {
          reservedType: word,
        },
      );
    }

    flowParseRestrictedIdentifierName(
      liberal?: boolean,
      declaration?: boolean,
    ): string {
      this.checkReservedType(
        this.state.value,
        this.state.startLoc,
        declaration,
      );
      return this.parseIdentifierName(liberal);
    }

    flowParseRestrictedIdentifier(
      liberal?: boolean,
      declaration?: boolean,
    ): N.Identifier {
      const node = this.startNode<N.Identifier>();
      const name = this.flowParseRestrictedIdentifierName(liberal, declaration);
      return this.createIdentifier(node, name);
    }

    // Type aliases

    flowParseTypeAlias(node: Undone<N.TypeAlias>): N.TypeAlias {
      node.id = this.flowParseRestrictedIdentifier(
        /* liberal */ false,
        /* declaration */ true,
      );
      this.scope.declareName(
        node.id.name,
        BindingFlag.TYPE_LEXICAL,
        node.id.start!,
      );

      if (this.match(tt.lt)) {
        node.typeParameters = this.flowParseTypeParameterDeclaration();
      } else {
        node.typeParameters = null;
      }

      node.right = this.flowParseTypeInitialiser(tt.eq);
      this.semicolon();

      return this.finishNode(node, "TypeAlias");
    }

    flowParseOpaqueType<T extends N.OpaqueType | N.DeclareOpaqueType>(
      node: Undone<T>,
      declare: boolean,
    ): T {
      this.expectContextual(tt._type);
      node.id = this.flowParseRestrictedIdentifier(
        /* liberal */ true,
        /* declaration */ true,
      );
      this.scope.declareName(
        node.id.name,
        BindingFlag.TYPE_LEXICAL,
        node.id.start!,
      );

      if (this.match(tt.lt)) {
        node.typeParameters = this.flowParseTypeParameterDeclaration();
      } else {
        node.typeParameters = null;
      }

      // Parse the supertype
      node.supertype = null;
      if (this.match(tt.colon)) {
        node.supertype = this.flowParseTypeInitialiser(tt.colon);
      }

      node.impltype = null;
      if (!declare) {
        node.impltype = this.flowParseTypeInitialiser(tt.eq);
      }
      this.semicolon();

      return this.finishNode(
        node,
        declare ? "DeclareOpaqueType" : "OpaqueType",
      );
    }

    // Type annotations

    flowParseTypeParameterBound(): N.TypeAnnotation | undefined {
      if (this.match(tt.colon) || this.isContextual(tt._extends)) {
        const node = this.startNode<N.TypeAnnotation>();
        this.next();
        node.typeAnnotation = this.flowParseType();
        return this.finishNode(node, "TypeAnnotation");
      }
    }

    flowParseTypeParameter(requireDefault: boolean = false): N.TypeParameter {
      const nodeStartLoc = this.state.startLoc;

      const node = this.startNode<N.TypeParameter>();
      const variance = this.flowParseVariance();

      node.name = this.flowParseRestrictedIdentifierName();
      node.variance = variance;
      node.bound = this.flowParseTypeParameterBound();

      if (this.match(tt.eq)) {
        this.eat(tt.eq);
        node.default = this.flowParseType();
      } else {
        if (requireDefault) {
          this.raise(FlowErrors.MissingTypeParamDefault, nodeStartLoc);
        }
      }

      return this.finishNode(node, "TypeParameter");
    }

    flowParseTypeParameterDeclaration(): N.TypeParameterDeclaration {
      const oldInType = this.state.inType;
      const node = this.startNode<N.TypeParameterDeclaration>();
      node.params = [];

      this.state.inType = true;

      // istanbul ignore else: this condition is already checked at all call sites
      if (this.match(tt.lt) || this.match(tt.jsxTagStart)) {
        this.next();
      } else {
        this.unexpected();
      }

      let defaultRequired = false;

      do {
        const typeParameter = this.flowParseTypeParameter(defaultRequired);

        node.params.push(typeParameter);

        if (typeParameter.default) {
          defaultRequired = true;
        }

        if (!this.match(tt.gt)) {
          this.expect(tt.comma);
        }
      } while (!this.match(tt.gt));
      this.expect(tt.gt);

      this.state.inType = oldInType;

      return this.finishNode(node, "TypeParameterDeclaration");
    }

    // Parse in top level normal context if we are in a JSX context
    flowInTopLevelContext<T>(cb: () => T): T {
      if (this.curContext() !== tc.brace) {
        const oldContext = this.state.context;
        this.state.context = [oldContext[0]];
        try {
          return cb();
        } finally {
          this.state.context = oldContext;
        }
      } else {
        return cb();
      }
    }

    // Used when parsing type arguments from ES or JSX productions, where the first token
    // has been created without state.inType. Thus we need to re-scan the lt token.
    flowParseTypeParameterInstantiationInExpression():
      | N.TypeParameterInstantiation
      | undefined {
      if (this.reScan_lt() !== tt.lt) return;
      return this.flowParseTypeParameterInstantiation();
    }

    flowParseTypeParameterInstantiation(): N.TypeParameterInstantiation {
      const node = this.startNode<N.TypeParameterInstantiation>();
      const oldInType = this.state.inType;

      this.state.inType = true;
      node.params = [];
      this.flowInTopLevelContext(() => {
        this.expect(tt.lt);
        const oldNoAnonFunctionType = this.state.noAnonFunctionType;
        this.state.noAnonFunctionType = false;
        while (!this.match(tt.gt)) {
          node.params.push(this.flowParseType());
          if (!this.match(tt.gt)) {
            this.expect(tt.comma);
          }
        }
        this.state.noAnonFunctionType = oldNoAnonFunctionType;
      });

      this.state.inType = oldInType;
      if (!this.state.inType && this.curContext() === tc.brace) {
        // rescan `>` when we are no longer in type context and JSX parsing context
        // since it was tokenized when `inType` is `true`.
        this.reScan_lt_gt();
      }
      this.expect(tt.gt);

      return this.finishNode(node, "TypeParameterInstantiation");
    }

    flowParseTypeParameterInstantiationCallOrNew(): N.TypeParameterInstantiation | null {
      if (this.reScan_lt() !== tt.lt) return null;
      const node = this.startNode<N.TypeParameterInstantiation>();
      const oldInType = this.state.inType;
      node.params = [];

      this.state.inType = true;

      this.expect(tt.lt);
      while (!this.match(tt.gt)) {
        node.params.push(this.flowParseTypeOrImplicitInstantiation());
        if (!this.match(tt.gt)) {
          this.expect(tt.comma);
        }
      }
      this.expect(tt.gt);

      this.state.inType = oldInType;

      return this.finishNode(node, "TypeParameterInstantiation");
    }

    flowParseInterfaceType(): N.InterfaceTypeAnnotation {
      const node = this.startNode<N.InterfaceTypeAnnotation>();
      this.expectContextual(tt._interface);

      node.extends = [];
      if (this.eat(tt._extends)) {
        do {
          node.extends.push(this.flowParseInterfaceExtends());
        } while (this.eat(tt.comma));
      }

      node.body = this.flowParseObjectType({
        allowStatic: false,
        allowExact: false,
        allowSpread: false,
        allowProto: false,
        allowInexact: false,
      });

      return this.finishNode(node, "InterfaceTypeAnnotation");
    }

    flowParseObjectPropertyKey() {
      return this.match(tt.num) || this.match(tt.string)
        ? (super.parseExprAtom() as N.NumericLiteral | N.StringLiteral)
        : this.parseIdentifier(true);
    }

    flowParseObjectTypeIndexer(
      node: Undone<N.ObjectTypeIndexer>,
      isStatic: boolean,
      variance?: N.Variance | null,
    ): N.ObjectTypeIndexer {
      node.static = isStatic;

      // Note: bracketL has already been consumed
      if (this.lookahead().type === tt.colon) {
        node.id = this.parseIdentifier(true);
        node.key = this.flowParseTypeInitialiser();
      } else {
        node.id = null;
        node.key = this.flowParseType();
      }
      this.expect(tt.bracketR);
      node.value = this.flowParseTypeInitialiser();
      node.variance = variance;

      return this.finishNode(node, "ObjectTypeIndexer");
    }

    flowParseObjectTypeInternalSlot(
      node: Undone<N.ObjectTypeInternalSlot>,
      isStatic: boolean,
    ): N.ObjectTypeInternalSlot {
      node.static = isStatic;
      // Note: both bracketL have already been consumed
      node.id = this.parseIdentifier(true);
      this.expect(tt.bracketR);
      this.expect(tt.bracketR);
      if (this.match(tt.lt) || this.match(tt.parenL)) {
        node.method = true;
        node.optional = false;
        node.value = this.flowParseObjectTypeMethodish(
          this.startNodeAtNode(node),
        );
      } else {
        node.method = false;
        if (this.eat(tt.question)) {
          node.optional = true;
        }
        node.value = this.flowParseTypeInitialiser();
      }
      return this.finishNode(node, "ObjectTypeInternalSlot");
    }

    flowParseObjectTypeMethodish(
      node: Undone<N.FunctionTypeAnnotation>,
    ): N.FunctionTypeAnnotation {
      node.params = [];
      node.rest = null;
      node.typeParameters = null;
      node.this = null;

      if (this.match(tt.lt)) {
        node.typeParameters = this.flowParseTypeParameterDeclaration();
      }

      this.expect(tt.parenL);
      if (this.match(tt._this)) {
        node.this = this.flowParseFunctionTypeParam(/* first */ true);
        // match Flow parser behavior
        node.this.name = null;
        if (!this.match(tt.parenR)) {
          this.expect(tt.comma);
        }
      }
      while (!this.match(tt.parenR) && !this.match(tt.ellipsis)) {
        node.params.push(this.flowParseFunctionTypeParam(false));
        if (!this.match(tt.parenR)) {
          this.expect(tt.comma);
        }
      }

      if (this.eat(tt.ellipsis)) {
        node.rest = this.flowParseFunctionTypeParam(false);
      }
      this.expect(tt.parenR);
      node.returnType = this.flowParseTypeInitialiser();

      return this.finishNode(node, "FunctionTypeAnnotation");
    }

    flowParseObjectTypeCallProperty(
      node: Undone<N.ObjectTypeCallProperty>,
      isStatic: boolean,
    ): N.ObjectTypeCallProperty {
      const valueNode = this.startNode();
      node.static = isStatic;
      node.value = this.flowParseObjectTypeMethodish(valueNode);
      return this.finishNode(node, "ObjectTypeCallProperty");
    }

    flowParseObjectType({
      allowStatic,
      allowExact,
      allowSpread,
      allowProto,
      allowInexact,
    }: {
      allowStatic: boolean;
      allowExact: boolean;
      allowSpread: boolean;
      allowProto: boolean;
      allowInexact: boolean;
    }): N.ObjectTypeAnnotation {
      const oldInType = this.state.inType;
      this.state.inType = true;

      const nodeStart = this.startNode<N.ObjectTypeAnnotation>();

      nodeStart.callProperties = [];
      nodeStart.properties = [];
      nodeStart.indexers = [];
      nodeStart.internalSlots = [];

      let endDelim;
      let exact;
      let inexact = false;
      if (allowExact && this.match(tt.braceBarL)) {
        this.expect(tt.braceBarL);
        endDelim = tt.braceBarR;
        exact = true;
      } else {
        this.expect(tt.braceL);
        endDelim = tt.braceR;
        exact = false;
      }

      nodeStart.exact = exact;

      while (!this.match(endDelim)) {
        let isStatic = false;
        let protoStartLoc: Position | undefined | null = null;
        let inexactStartLoc: Position | undefined | null = null;
        const node = this.startNode();

        if (allowProto && this.isContextual(tt._proto)) {
          const lookahead = this.lookahead();

          if (lookahead.type !== tt.colon && lookahead.type !== tt.question) {
            this.next();
            protoStartLoc = this.state.startLoc;
            allowStatic = false;
          }
        }

        if (allowStatic && this.isContextual(tt._static)) {
          const lookahead = this.lookahead();

          // static is a valid identifier name
          if (lookahead.type !== tt.colon && lookahead.type !== tt.question) {
            this.next();
            isStatic = true;
          }
        }

        const variance = this.flowParseVariance();

        if (this.eat(tt.bracketL)) {
          if (protoStartLoc != null) {
            this.unexpected(protoStartLoc);
          }
          if (this.eat(tt.bracketL)) {
            if (variance) {
              this.unexpected(variance.start);
            }
            nodeStart.internalSlots.push(
              this.flowParseObjectTypeInternalSlot(node, isStatic),
            );
          } else {
            nodeStart.indexers.push(
              this.flowParseObjectTypeIndexer(node, isStatic, variance),
            );
          }
        } else if (this.match(tt.parenL) || this.match(tt.lt)) {
          if (protoStartLoc != null) {
            this.unexpected(protoStartLoc);
          }
          if (variance) {
            this.unexpected(variance.start);
          }
          nodeStart.callProperties.push(
            this.flowParseObjectTypeCallProperty(node, isStatic),
          );
        } else {
          let kind: N.ObjectTypeProperty["kind"] = "init";

          if (this.isContextual(tt._get) || this.isContextual(tt._set)) {
            const lookahead = this.lookahead();
            if (tokenIsLiteralPropertyName(lookahead.type)) {
              kind = this.state.value as "get" | "set";
              this.next();
            }
          }

          const propOrInexact = this.flowParseObjectTypeProperty(
            node,
            isStatic,
            protoStartLoc,
            variance,
            kind,
            allowSpread,
            allowInexact ?? !exact,
          );

          if (propOrInexact === null) {
            inexact = true;
            inexactStartLoc = this.state.lastTokStartLoc;
          } else {
            nodeStart.properties.push(propOrInexact);
          }
        }

        this.flowObjectTypeSemicolon();

        if (
          inexactStartLoc &&
          !this.match(tt.braceR) &&
          !this.match(tt.braceBarR)
        ) {
          this.raise(
            FlowErrors.UnexpectedExplicitInexactInObject,
            inexactStartLoc,
          );
        }
      }

      this.expect(endDelim);

      /* The inexact flag should only be added on ObjectTypeAnnotations that
       * are not the body of an interface, declare interface, or declare class.
       * Since spreads are only allowed in object types, checking that is
       * sufficient here.
       */
      if (allowSpread) {
        nodeStart.inexact = inexact;
      }

      const out = this.finishNode(nodeStart, "ObjectTypeAnnotation");

      this.state.inType = oldInType;

      return out;
    }

    flowParseObjectTypeProperty(
      node: Undone<N.ObjectTypeProperty | N.ObjectTypeSpreadProperty>,
      isStatic: boolean,
      protoStartLoc: Position | undefined | null,
      variance: N.Variance | undefined | null,
      kind: N.ObjectTypeProperty["kind"],
      allowSpread: boolean,
      allowInexact: boolean,
    ): N.ObjectTypeProperty | N.ObjectTypeSpreadProperty | null {
      if (this.eat(tt.ellipsis)) {
        const isInexactToken =
          this.match(tt.comma) ||
          this.match(tt.semi) ||
          this.match(tt.braceR) ||
          this.match(tt.braceBarR);

        if (isInexactToken) {
          if (!allowSpread) {
            this.raise(
              FlowErrors.InexactInsideNonObject,
              this.state.lastTokStartLoc!,
            );
          } else if (!allowInexact) {
            this.raise(
              FlowErrors.InexactInsideExact,
              this.state.lastTokStartLoc!,
            );
          }
          if (variance) {
            this.raise(FlowErrors.InexactVariance, variance);
          }

          return null;
        }

        if (!allowSpread) {
          this.raise(
            FlowErrors.UnexpectedSpreadType,
            this.state.lastTokStartLoc!,
          );
        }
        if (protoStartLoc != null) {
          this.unexpected(protoStartLoc);
        }
        if (variance) {
          this.raise(FlowErrors.SpreadVariance, variance);
        }

        (node as Undone<N.ObjectTypeSpreadProperty>).argument =
          this.flowParseType();
        return this.finishNode(node, "ObjectTypeSpreadProperty");
      } else {
        (node as Undone<N.ObjectTypeProperty>).key =
          this.flowParseObjectPropertyKey();
        (node as Undone<N.ObjectTypeProperty>).static = isStatic;
        (node as Undone<N.ObjectTypeProperty>).proto = protoStartLoc != null;
        (node as Undone<N.ObjectTypeProperty>).kind = kind;

        let optional = false;
        if (this.match(tt.lt) || this.match(tt.parenL)) {
          // This is a method property
          (node as Undone<N.ObjectTypeProperty>).method = true;

          if (protoStartLoc != null) {
            this.unexpected(protoStartLoc);
          }
          if (variance) {
            this.unexpected(variance.start);
          }

          (node as Undone<N.ObjectTypeProperty>).value =
            this.flowParseObjectTypeMethodish(this.startNodeAtNode(node));
          if (kind === "get" || kind === "set") {
            this.flowCheckGetterSetterParams(
              node as Undone<N.ObjectTypeProperty>,
            );
          } else if (
            !isStatic &&
            /** Declared classes/interfaces do not allow spread */
            !allowSpread &&
            // @ts-expect-error name is not define in StringLiteral
            (node as Undone<N.ObjectTypeProperty>).key.name === "constructor" &&
            (
              (node as Undone<N.ObjectTypeProperty>)
                .value as N.FunctionTypeAnnotation
            ).this
          ) {
            this.raise(
              FlowErrors.ThisParamBannedInConstructor,
              (
                (node as Undone<N.ObjectTypeProperty>)
                  .value as N.FunctionTypeAnnotation
              ).this!,
            );
          }
        } else {
          if (kind !== "init") this.unexpected();

          (node as Undone<N.ObjectTypeProperty>).method = false;

          if (this.eat(tt.question)) {
            optional = true;
          }
          (node as Undone<N.ObjectTypeProperty>).value =
            this.flowParseTypeInitialiser();
          (node as Undone<N.ObjectTypeProperty>).variance = variance;
        }

        (node as Undone<N.ObjectTypeProperty>).optional = optional;
        return this.finishNode(node, "ObjectTypeProperty");
      }
    }

    // This is similar to checkGetterSetterParams, but as
    // @babel/parser uses non estree properties we cannot reuse it here
    flowCheckGetterSetterParams(property: Undone<N.ObjectTypeProperty>): void {
      const paramCount = property.kind === "get" ? 0 : 1;
      const value = property.value as N.FunctionTypeAnnotation;
      const length = value.params.length + (value.rest ? 1 : 0);

      if (value.this) {
        this.raise(
          property.kind === "get"
            ? FlowErrors.GetterMayNotHaveThisParam
            : FlowErrors.SetterMayNotHaveThisParam,
          value.this,
        );
      }

      if (length !== paramCount) {
        this.raise(
          property.kind === "get"
            ? Errors.BadGetterArity
            : Errors.BadSetterArity,
          property,
        );
      }

      if (property.kind === "set" && value.rest) {
        this.raise(Errors.BadSetterRestParameter, property);
      }
    }

    flowObjectTypeSemicolon(): void {
      if (
        !this.eat(tt.semi) &&
        !this.eat(tt.comma) &&
        !this.match(tt.braceR) &&
        !this.match(tt.braceBarR)
      ) {
        this.unexpected();
      }
    }

    flowParseQualifiedTypeIdentifier(
      startLoc?: Position,
      id?: N.Identifier,
    ): N.QualifiedTypeIdentifier | N.Identifier {
      startLoc ??= this.state.startLoc;
      let node: N.Identifier | N.QualifiedTypeIdentifier =
        id || this.flowParseRestrictedIdentifier(true);

      while (this.eat(tt.dot)) {
        const node2 = this.startNodeAt<N.QualifiedTypeIdentifier>(startLoc);
        node2.qualification = node;
        node2.id = this.flowParseRestrictedIdentifier(true);
        node = this.finishNode(node2, "QualifiedTypeIdentifier");
      }

      return node;
    }

    flowParseGenericType(
      startLoc: Position,
      id: N.Identifier,
    ): N.GenericTypeAnnotation {
      const node = this.startNodeAt<N.GenericTypeAnnotation>(startLoc);

      node.typeParameters = null;
      node.id = this.flowParseQualifiedTypeIdentifier(startLoc, id);

      if (this.match(tt.lt)) {
        node.typeParameters = this.flowParseTypeParameterInstantiation();
      }

      return this.finishNode(node, "GenericTypeAnnotation");
    }

    flowParseTypeofType(): N.TypeofTypeAnnotation {
      const node = this.startNode<N.TypeofTypeAnnotation>();
      this.expect(tt._typeof);
      node.argument = this.flowParsePrimaryType();
      return this.finishNode(node, "TypeofTypeAnnotation");
    }

    flowParseTupleType(): N.TupleTypeAnnotation {
      const node = this.startNode<N.TupleTypeAnnotation>();
      node.types = [];
      this.expect(tt.bracketL);
      // We allow trailing commas
      while (this.state.pos < this.length && !this.match(tt.bracketR)) {
        node.types.push(this.flowParseType());
        if (this.match(tt.bracketR)) break;
        this.expect(tt.comma);
      }
      this.expect(tt.bracketR);
      return this.finishNode(node, "TupleTypeAnnotation");
    }

    flowParseFunctionTypeParam(first: boolean): N.FunctionTypeParam {
      let name = null;
      let optional = false;
      let typeAnnotation: N.FlowType;
      const node = this.startNode<N.FunctionTypeParam>();
      const lh = this.lookahead();
      const isThis = this.state.type === tt._this;

      if (lh.type === tt.colon || lh.type === tt.question) {
        if (isThis && !first) {
          this.raise(FlowErrors.ThisParamMustBeFirst, node);
        }
        name = this.parseIdentifier(isThis);
        if (this.eat(tt.question)) {
          optional = true;
          if (isThis) {
            this.raise(FlowErrors.ThisParamMayNotBeOptional, node);
          }
        }
        typeAnnotation = this.flowParseTypeInitialiser();
      } else {
        typeAnnotation = this.flowParseType();
      }
      node.name = name;
      node.optional = optional;
      node.typeAnnotation = typeAnnotation;
      return this.finishNode(node, "FunctionTypeParam");
    }

    reinterpretTypeAsFunctionTypeParam(type: N.FlowType): N.FunctionTypeParam {
      const node = this.startNodeAtNode<N.FunctionTypeParam>(type);
      node.name = null;
      node.optional = false;
      node.typeAnnotation = type;
      return this.finishNode(node, "FunctionTypeParam");
    }

    flowParseFunctionTypeParams(params: N.FunctionTypeParam[] = []): {
      params: N.FunctionTypeParam[];
      rest: N.FunctionTypeParam | undefined | null;
      _this: N.FunctionTypeParam | undefined | null;
    } {
      let rest: N.FunctionTypeParam | undefined | null = null;
      let _this: N.FunctionTypeParam | undefined | null = null;
      if (this.match(tt._this)) {
        _this = this.flowParseFunctionTypeParam(/* first */ true);
        // match Flow parser behavior
        _this.name = null;
        if (!this.match(tt.parenR)) {
          this.expect(tt.comma);
        }
      }
      while (!this.match(tt.parenR) && !this.match(tt.ellipsis)) {
        params.push(this.flowParseFunctionTypeParam(false));
        if (!this.match(tt.parenR)) {
          this.expect(tt.comma);
        }
      }
      if (this.eat(tt.ellipsis)) {
        rest = this.flowParseFunctionTypeParam(false);
      }
      return { params, rest, _this };
    }

    flowIdentToTypeAnnotation(
      startLoc: Position,
      node: Undone<N.FlowType>,
      id: N.Identifier,
    ): N.FlowType {
      switch (id.name) {
        case "any":
          return this.finishNode(node, "AnyTypeAnnotation");

        case "bool":
        case "boolean":
          return this.finishNode(node, "BooleanTypeAnnotation");

        case "mixed":
          return this.finishNode(node, "MixedTypeAnnotation");

        case "empty":
          return this.finishNode(node, "EmptyTypeAnnotation");

        case "number":
          return this.finishNode(node, "NumberTypeAnnotation");

        case "string":
          return this.finishNode(node, "StringTypeAnnotation");

        case "symbol":
          return this.finishNode(node, "SymbolTypeAnnotation");

        default:
          this.checkNotUnderscore(id.name);
          return this.flowParseGenericType(startLoc, id);
      }
    }

    // The parsing of types roughly parallels the parsing of expressions, and
    // primary types are kind of like primary expressions...they're the
    // primitives with which other types are constructed.
    flowParsePrimaryType(): N.FlowType {
      const startLoc = this.state.startLoc;
      const node = this.startNode<N.FlowOtherTypeAnnotation>();
      let tmp;
      let type;
      let isGroupedType = false;
      const oldNoAnonFunctionType = this.state.noAnonFunctionType;

      switch (this.state.type) {
        case tt.braceL:
          return this.flowParseObjectType({
            allowStatic: false,
            allowExact: false,
            allowSpread: true,
            allowProto: false,
            allowInexact: true,
          });

        case tt.braceBarL:
          return this.flowParseObjectType({
            allowStatic: false,
            allowExact: true,
            allowSpread: true,
            allowProto: false,
            allowInexact: false,
          });

        case tt.bracketL:
          this.state.noAnonFunctionType = false;
          type = this.flowParseTupleType();
          this.state.noAnonFunctionType = oldNoAnonFunctionType;
          return type;

        case tt.lt: {
          const node = this.startNode<N.FunctionTypeAnnotation>();
          node.typeParameters = this.flowParseTypeParameterDeclaration();
          this.expect(tt.parenL);
          tmp = this.flowParseFunctionTypeParams();
          node.params = tmp.params;
          node.rest = tmp.rest;
          node.this = tmp._this;
          this.expect(tt.parenR);

          this.expect(tt.arrow);

          node.returnType = this.flowParseType();

          return this.finishNode(node, "FunctionTypeAnnotation");
        }

        case tt.parenL: {
          const node = this.startNode<N.FunctionTypeAnnotation>();
          this.next();

          // Check to see if this is actually a grouped type
          if (!this.match(tt.parenR) && !this.match(tt.ellipsis)) {
            if (tokenIsIdentifier(this.state.type) || this.match(tt._this)) {
              const token = this.lookahead().type;
              isGroupedType = token !== tt.question && token !== tt.colon;
            } else {
              isGroupedType = true;
            }
          }

          if (isGroupedType) {
            this.state.noAnonFunctionType = false;
            type = this.flowParseType();
            this.state.noAnonFunctionType = oldNoAnonFunctionType;

            // A `,` or a `) =>` means this is an anonymous function type
            if (
              this.state.noAnonFunctionType ||
              !(
                this.match(tt.comma) ||
                (this.match(tt.parenR) && this.lookahead().type === tt.arrow)
              )
            ) {
              this.expect(tt.parenR);
              return type;
            } else {
              // Eat a comma if there is one
              this.eat(tt.comma);
            }
          }

          if (type) {
            tmp = this.flowParseFunctionTypeParams([
              this.reinterpretTypeAsFunctionTypeParam(type),
            ]);
          } else {
            tmp = this.flowParseFunctionTypeParams();
          }

          node.params = tmp.params;
          node.rest = tmp.rest;
          node.this = tmp._this;

          this.expect(tt.parenR);

          this.expect(tt.arrow);

          node.returnType = this.flowParseType();

          node.typeParameters = null;

          return this.finishNode(node, "FunctionTypeAnnotation");
        }

        case tt.string:
          return this.parseLiteral<N.StringLiteralTypeAnnotation>(
            this.state.value,
            "StringLiteralTypeAnnotation",
          );

        case tt._true:
        case tt._false:
          (node as Undone<N.BooleanLiteralTypeAnnotation>).value = this.match(
            tt._true,
          );
          this.next();
          return this.finishNode(
            node as Undone<N.BooleanLiteralTypeAnnotation>,
            "BooleanLiteralTypeAnnotation",
          );

        case tt.plusMin:
          if (this.state.value === "-") {
            this.next();
            if (this.match(tt.num)) {
              return this.parseLiteralAtNode<N.NumberLiteralTypeAnnotation>(
                -this.state.value,
                "NumberLiteralTypeAnnotation",
                node,
              );
            }

            if (this.match(tt.bigint)) {
              return this.parseLiteralAtNode<N.BigIntLiteralTypeAnnotation>(
                -this.state.value,
                "BigIntLiteralTypeAnnotation",
                node,
              );
            }

            throw this.raise(
              FlowErrors.UnexpectedSubtractionOperand,
              this.state.startLoc,
            );
          }
          throw this.unexpected();
        case tt.num:
          return this.parseLiteral(
            this.state.value,
            "NumberLiteralTypeAnnotation",
          );

        case tt.bigint:
          return this.parseLiteral(
            this.state.value,
            "BigIntLiteralTypeAnnotation",
          );

        case tt._void:
          this.next();
          return this.finishNode(node, "VoidTypeAnnotation");

        case tt._null:
          this.next();
          return this.finishNode(node, "NullLiteralTypeAnnotation");

        case tt._this:
          this.next();
          return this.finishNode(node, "ThisTypeAnnotation");

        case tt.star:
          this.next();
          return this.finishNode(node, "ExistsTypeAnnotation");

        case tt._typeof:
          return this.flowParseTypeofType();

        default:
          if (tokenIsKeyword(this.state.type)) {
            const label = tokenLabelName(this.state.type);
            this.next();
            // @ts-expect-error this function returns a FlowType, but we return an Identifier here
            // Consider apply flowIdentToTypeAnnotation to the identifier
            return super.createIdentifier(node as Undone<N.Identifier>, label);
          } else if (tokenIsIdentifier(this.state.type)) {
            if (this.isContextual(tt._interface)) {
              return this.flowParseInterfaceType();
            }

            return this.flowIdentToTypeAnnotation(
              startLoc,
              node,
              this.parseIdentifier(),
            );
          }
      }

      throw this.unexpected();
    }

    flowParsePostfixType(): N.FlowType {
      const startLoc = this.state.startLoc;
      let type = this.flowParsePrimaryType();
      let seenOptionalIndexedAccess = false;
      while (
        (this.match(tt.bracketL) || this.match(tt.questionDot)) &&
        !this.canInsertSemicolon()
      ) {
        const node = this.startNodeAt<N.FlowOtherTypeAnnotation>(startLoc);
        const optional = this.eat(tt.questionDot);
        seenOptionalIndexedAccess = seenOptionalIndexedAccess || optional;
        this.expect(tt.bracketL);
        if (!optional && this.match(tt.bracketR)) {
          (node as Undone<N.ArrayTypeAnnotation>).elementType = type;
          this.next(); // eat `]`
          type = this.finishNode(node, "ArrayTypeAnnotation");
        } else {
          (node as Undone<N.IndexedAccessType>).objectType = type;
          (node as Undone<N.IndexedAccessType>).indexType =
            this.flowParseType();
          this.expect(tt.bracketR);
          if (seenOptionalIndexedAccess) {
            (node as Undone<N.OptionalIndexedAccessType>).optional = optional;
            type = this.finishNode(
              node as Undone<N.OptionalIndexedAccessType>,
              "OptionalIndexedAccessType",
            );
          } else {
            type = this.finishNode(
              node as Undone<N.IndexedAccessType>,
              "IndexedAccessType",
            );
          }
        }
      }
      return type;
    }

    flowParsePrefixType(): N.FlowType {
      const node = this.startNode<N.NullableTypeAnnotation>();
      if (this.eat(tt.question)) {
        node.typeAnnotation = this.flowParsePrefixType();
        return this.finishNode(node, "NullableTypeAnnotation");
      } else {
        return this.flowParsePostfixType();
      }
    }

    flowParseAnonFunctionWithoutParens(): N.FlowType {
      const param = this.flowParsePrefixType();
      if (!this.state.noAnonFunctionType && this.eat(tt.arrow)) {
        // TODO: This should be a type error. Passing in a SourceLocation, and it expects a Position.
        const node = this.startNodeAtNode<N.FunctionTypeAnnotation>(param);
        node.params = [this.reinterpretTypeAsFunctionTypeParam(param)];
        node.rest = null;
        node.this = null;
        node.returnType = this.flowParseType();
        node.typeParameters = null;
        return this.finishNode(node, "FunctionTypeAnnotation");
      }
      return param;
    }

    flowParseIntersectionType(): N.FlowType {
      const node = this.startNode<N.IntersectionTypeAnnotation>();
      this.eat(tt.bitwiseAND);
      const type = this.flowParseAnonFunctionWithoutParens();
      node.types = [type];
      while (this.eat(tt.bitwiseAND)) {
        node.types.push(this.flowParseAnonFunctionWithoutParens());
      }
      return node.types.length === 1
        ? type
        : this.finishNode(node, "IntersectionTypeAnnotation");
    }

    flowParseUnionType(): N.FlowType {
      const node = this.startNode<N.UnionTypeAnnotation>();
      this.eat(tt.bitwiseOR);
      const type = this.flowParseIntersectionType();
      node.types = [type];
      while (this.eat(tt.bitwiseOR)) {
        node.types.push(this.flowParseIntersectionType());
      }
      return node.types.length === 1
        ? type
        : this.finishNode(node, "UnionTypeAnnotation");
    }

    flowParseType(): N.FlowType {
      const oldInType = this.state.inType;
      this.state.inType = true;
      const type = this.flowParseUnionType();
      this.state.inType = oldInType;
      return type;
    }

    flowParseTypeOrImplicitInstantiation(): N.FlowType {
      if (this.state.type === tt.name && this.state.value === "_") {
        const startLoc = this.state.startLoc;
        const node = this.parseIdentifier();
        return this.flowParseGenericType(startLoc, node);
      } else {
        return this.flowParseType();
      }
    }

    flowParseTypeAnnotation(): N.TypeAnnotation {
      const node = this.startNode<N.TypeAnnotation>();
      node.typeAnnotation = this.flowParseTypeInitialiser();
      return this.finishNode(node, "TypeAnnotation");
    }

    flowParseTypeAnnotatableIdentifier(): N.Identifier {
      const node = this.startNode<N.Identifier>();
      const name = this.parseIdentifierName();
      if (this.match(tt.colon)) {
        node.typeAnnotation = this.flowParseTypeAnnotation();
      }
      return this.createIdentifier(node, name);
    }

    typeCastToParameter(node: N.TypeCastExpression): N.Expression {
      (node.expression as N.Identifier).typeAnnotation = node.typeAnnotation;

      this.resetEndLocationFromNode(node.expression, node.typeAnnotation);

      return node.expression;
    }

    flowParseVariance(): N.Variance | undefined | null {
      let variance = null;
      if (this.match(tt.plusMin)) {
        variance = this.startNode<N.Variance>();
        if (this.state.value === "+") {
          variance.kind = "plus";
        } else {
          variance.kind = "minus";
        }
        this.next();
        return this.finishNode(variance, "Variance");
      }
      return variance;
    }

    // ==================================
    // Overrides
    // ==================================

    parseFunctionBody(
      node: N.Function,
      allowExpressionBody?: boolean | null,
      isMethod: boolean = false,
    ): void {
      if (allowExpressionBody) {
        this.forwardNoArrowParamsConversionAt(node, () =>
          super.parseFunctionBody(node, true, isMethod),
        );
        return;
      }

      super.parseFunctionBody(node, false, isMethod);
    }

    parseFunctionBodyAndFinish<
      T extends
        | N.Function
        | N.TSDeclareMethod
        | N.TSDeclareFunction
        | N.ClassPrivateMethod,
    >(node: Undone<T>, type: T["type"], isMethod: boolean = false): T {
      if (this.match(tt.colon)) {
        const typeNode = this.startNode<N.TypeAnnotation>();

        if (
          type === "FunctionDeclaration" ||
          type === "FunctionExpression" ||
          type === "ArrowFunctionExpression"
        ) {
          [
            typeNode.typeAnnotation,
            (
              node as Undone<
                | N.FunctionDeclaration
                | N.FunctionExpression
                | N.ArrowFunctionExpression
              >
            ).predicate,
          ] = this.flowParseTypeAndPredicateInitialiser(true) as [
            N.FlowType,
            N.FlowPredicate,
          ];
        } else {
          typeNode.typeAnnotation = this.flowParseTypeInitialiser();
        }

        node.returnType = typeNode.typeAnnotation
          ? this.finishNode(typeNode, "TypeAnnotation")
          : null;
      }

      return super.parseFunctionBodyAndFinish(node, type, isMethod);
    }

    // interfaces and enums
    parseStatementLike(flags: ParseStatementFlag): N.Statement {
      // strict mode handling of `interface` since it's a reserved word
      if (this.state.strict && this.isContextual(tt._interface)) {
        const lookahead = this.lookahead();
        if (tokenIsKeywordOrIdentifier(lookahead.type)) {
          const node = this.startNode<N.InterfaceDeclaration>();
          this.next();
          return this.flowParseInterface(node);
        }
      } else if (this.isContextual(tt._enum)) {
        const node = this.startNode<N.EnumDeclaration>();
        this.next();
        return this.flowParseEnumDeclaration(node);
      }
      const stmt = super.parseStatementLike(flags);
      // We will parse a flow pragma in any comment before the first statement.
      if (this.flowPragma === undefined && !this.isValidDirective(stmt)) {
        this.flowPragma = null;
      }
      return stmt;
    }

    // declares, interfaces and type aliases
    parseExpressionStatement(
      node: Undone<N.ExpressionStatement>,
      expr: N.Expression,
      decorators: N.Decorator[] | null,
    ): N.ExpressionStatement {
      if (expr.type === "Identifier") {
        if (expr.name === "declare") {
          if (
            this.match(tt._class) ||
            tokenIsIdentifier(this.state.type) ||
            this.match(tt._function) ||
            this.match(tt._var) ||
            this.match(tt._export)
          ) {
            // @ts-expect-error: refine typings
            return this.flowParseDeclare(node);
          }
        } else if (tokenIsIdentifier(this.state.type)) {
          if (expr.name === "interface") {
            // @ts-expect-error: refine typings
            return this.flowParseInterface(node);
          } else if (expr.name === "type") {
            // @ts-expect-error: refine typings
            return this.flowParseTypeAlias(node);
          } else if (expr.name === "opaque") {
            // @ts-expect-error: refine typings
            return this.flowParseOpaqueType(
              node as unknown as Undone<N.OpaqueType>,
              false,
            );
          }
        }
      }

      return super.parseExpressionStatement(node, expr, decorators);
    }

    // export type
    shouldParseExportDeclaration(): boolean {
      const { type } = this.state;
      if (type === tt._enum || tokenIsFlowInterfaceOrTypeOrOpaque(type)) {
        return !this.state.containsEsc;
      }
      return super.shouldParseExportDeclaration();
    }

    isExportDefaultSpecifier(): boolean {
      const { type } = this.state;
      if (type === tt._enum || tokenIsFlowInterfaceOrTypeOrOpaque(type)) {
        return this.state.containsEsc;
      }

      return super.isExportDefaultSpecifier();
    }

    parseExportDefaultExpression() {
      if (this.isContextual(tt._enum)) {
        const node = this.startNode();
        this.next();
        return this.flowParseEnumDeclaration(node);
      }
      return super.parseExportDefaultExpression();
    }

    parseConditional(
      expr: N.Expression,
      startLoc: Position,
      refExpressionErrors?: ExpressionErrors | null,
    ): N.Expression {
      if (!this.match(tt.question)) return expr;

      if (refExpressionErrors != null) {
        const nextCh = this.lookaheadCharCode();
        // These tokens cannot start an expression, so if one of them follows
        // ? then we are probably in an arrow function parameters list and we
        // don't parse the conditional expression.
        if (
          nextCh === charCodes.comma || // (a?, b) => c
          nextCh === charCodes.equalsTo || // (a? = b) => c
          nextCh === charCodes.colon || // (a?: b) => c
          nextCh === charCodes.rightParenthesis // (a?) => c
        ) {
          this.setOptionalParametersError(refExpressionErrors);
          return expr;
        }
      }

      this.expect(tt.question);
      const state = this.state.clone();
      const originalNoArrowAt = this.state.noArrowAt;
      const node = this.startNodeAt<N.ConditionalExpression>(startLoc);
      let { consequent, failed } = this.tryParseConditionalConsequent();
      const result = this.getArrowLikeExpressions(consequent);
      let valid = result[0];
      const invalid = result[1];

      if (failed || invalid.length > 0) {
        const noArrowAt = [...originalNoArrowAt];

        if (invalid.length > 0) {
          this.state = state;
          this.state.noArrowAt = noArrowAt;

          for (let i = 0; i < invalid.length; i++) {
            noArrowAt.push(invalid[i].start!);
          }

          ({ consequent, failed } = this.tryParseConditionalConsequent());
          [valid] = this.getArrowLikeExpressions(consequent);
        }

        if (failed && valid.length > 1) {
          // if there are two or more possible correct ways of parsing, throw an
          // error.
          // e.g.   Source: a ? (b): c => (d): e => f
          //      Result 1: a ? b : (c => ((d): e => f))
          //      Result 2: a ? ((b): c => d) : (e => f)
          this.raise(FlowErrors.AmbiguousConditionalArrow, state.startLoc);
        }

        if (failed && valid.length === 1) {
          this.state = state;
          noArrowAt.push(valid[0].start!);
          this.state.noArrowAt = noArrowAt;
          ({ consequent } = this.tryParseConditionalConsequent());
        }
      }

      this.getArrowLikeExpressions(consequent, true);

      this.state.noArrowAt = originalNoArrowAt;
      this.expect(tt.colon);

      node.test = expr;
      node.consequent = consequent;
      node.alternate = this.forwardNoArrowParamsConversionAt(node, () =>
        this.parseMaybeAssign(undefined, undefined),
      );

      return this.finishNode(node, "ConditionalExpression");
    }

    tryParseConditionalConsequent(): {
      consequent: N.Expression;
      failed: boolean;
    } {
      this.state.noArrowParamsConversionAt.push(this.state.start);

      const consequent = this.parseMaybeAssignAllowIn();
      const failed = !this.match(tt.colon);

      this.state.noArrowParamsConversionAt.pop();

      return { consequent, failed };
    }

    // Given an expression, walks through out its arrow functions whose body is
    // an expression and through out conditional expressions. It returns every
    // function which has been parsed with a return type but could have been
    // parenthesized expressions.
    // These functions are separated into two arrays: one containing the ones
    // whose parameters can be converted to assignable lists, one containing the
    // others.
    getArrowLikeExpressions(
      node: N.Expression,
      disallowInvalid?: boolean,
    ): [N.ArrowFunctionExpression[], N.ArrowFunctionExpression[]] {
      const stack = [node];
      const arrows: N.ArrowFunctionExpression[] = [];

      while (stack.length !== 0) {
        const node = stack.pop()!;
        if (
          node.type === "ArrowFunctionExpression" &&
          node.body.type !== "BlockStatement"
        ) {
          if (node.typeParameters || !node.returnType) {
            // This is an arrow expression without ambiguity, so check its parameters
            this.finishArrowValidation(node);
          } else {
            arrows.push(node);
          }
          stack.push(node.body);
        } else if (node.type === "ConditionalExpression") {
          stack.push(node.consequent);
          stack.push(node.alternate);
        }
      }

      if (disallowInvalid) {
        arrows.forEach(node => this.finishArrowValidation(node));
        return [arrows, []];
      }

      return partition(arrows, node =>
        node.params.every(param => this.isAssignable(param, true)),
      );
    }

    finishArrowValidation(node: N.ArrowFunctionExpression) {
      this.toAssignableList(
        // node.params is Expression[] instead of $ReadOnlyArray<Pattern> because it
        // has not been converted yet.
        node.params as any as N.Expression[],
        node.extra?.trailingCommaLoc as Position,
        /* isLHS */ false,
      );
      // Enter scope, as checkParams defines bindings
      this.scope.enter(ScopeFlag.FUNCTION | ScopeFlag.ARROW);
      // Use super's method to force the parameters to be checked
      super.checkParams(node, false, true);
      this.scope.exit();
    }

    forwardNoArrowParamsConversionAt<T>(
      node: Undone<N.Node>,
      parse: () => T,
    ): T {
      let result: T;
      if (
        this.state.noArrowParamsConversionAt.includes(
          this.offsetToSourcePos(node.start!),
        )
      ) {
        this.state.noArrowParamsConversionAt.push(this.state.start);
        result = parse();
        this.state.noArrowParamsConversionAt.pop();
      } else {
        result = parse();
      }

      return result;
    }

    parseParenItem<
      T extends
        | N.Expression
        | N.RestElement
        | N.SpreadElement
        | N.TSTypeCastExpression
        | N.TypeCastExpression,
    >(
      node: T,
      startLoc: Position,
    ): T | N.TypeCastExpression | N.TSTypeCastExpression {
      const newNode = super.parseParenItem(node, startLoc);
      if (this.eat(tt.question)) {
        (newNode as N.Identifier).optional = true;
        // Include question mark in location of node
        // Don't use this.finishNode() as otherwise we might process comments twice and
        // include already consumed parens
        this.resetEndLocation(node);
      }

      if (this.match(tt.colon)) {
        const typeCastNode = this.startNodeAt<N.TypeCastExpression>(startLoc);
        typeCastNode.expression = newNode as N.Expression;
        typeCastNode.typeAnnotation = this.flowParseTypeAnnotation();

        return this.finishNode(typeCastNode, "TypeCastExpression");
      }

      return newNode;
    }

    assertModuleNodeAllowed(node: N.Node) {
      if (
        (node.type === "ImportDeclaration" &&
          (node.importKind === "type" || node.importKind === "typeof")) ||
        (node.type === "ExportNamedDeclaration" &&
          node.exportKind === "type") ||
        (node.type === "ExportAllDeclaration" && node.exportKind === "type")
      ) {
        // Allow Flowtype imports and exports in all conditions because
        // Flow itself does not care about 'sourceType'.
        return;
      }

      super.assertModuleNodeAllowed(node);
    }

    parseExportDeclaration(
      node: N.ExportNamedDeclaration,
    ): N.ExportNamedDeclaration["declaration"] | undefined {
      if (this.isContextual(tt._type)) {
        node.exportKind = "type";

        const declarationNode = this.startNode();
        this.next();

        if (this.match(tt.braceL)) {
          // export type { foo, bar };
          node.specifiers = this.parseExportSpecifiers(
            /* isInTypeExport */ true,
          );
          super.parseExportFrom(node);
          return null;
        } else {
          // export type Foo = Bar;
          return this.flowParseTypeAlias(declarationNode);
        }
      } else if (this.isContextual(tt._opaque)) {
        node.exportKind = "type";

        const declarationNode = this.startNode<N.OpaqueType>();
        this.next();
        // export opaque type Foo = Bar;
        return this.flowParseOpaqueType(declarationNode, false);
      } else if (this.isContextual(tt._interface)) {
        node.exportKind = "type";
        const declarationNode = this.startNode();
        this.next();
        return this.flowParseInterface(declarationNode);
      } else if (this.isContextual(tt._enum)) {
        node.exportKind = "value";
        const declarationNode = this.startNode();
        this.next();
        return this.flowParseEnumDeclaration(declarationNode);
      } else {
        return super.parseExportDeclaration(node);
      }
    }

    eatExportStar(
      node: Undone<N.Node>,
    ): node is Undone<N.ExportNamedDeclaration | N.ExportAllDeclaration> {
      if (super.eatExportStar(node)) return true;

      if (this.isContextual(tt._type) && this.lookahead().type === tt.star) {
        (
          node as Undone<N.ExportNamedDeclaration | N.ExportAllDeclaration>
        ).exportKind = "type";
        this.next();
        this.next();
        return true;
      }

      return false;
    }

    maybeParseExportNamespaceSpecifier(
      node: Undone<N.ExportNamedDeclaration | N.ExportAllDeclaration>,
    ): node is Undone<N.ExportNamedDeclaration> {
      const { startLoc } = this.state;
      const hasNamespace = super.maybeParseExportNamespaceSpecifier(node);
      if (hasNamespace && node.exportKind === "type") {
        this.unexpected(startLoc);
      }
      return hasNamespace;
    }

    parseClassId(
      node: N.Class,
      isStatement: boolean,
      optionalId?: boolean | null,
    ) {
      if ((!isStatement || optionalId) && this.isContextual(tt._implements)) {
        node.id = null;
        return;
      }
      super.parseClassId(node, isStatement, optionalId);
      if (this.match(tt.lt)) {
        node.typeParameters = this.flowParseTypeParameterDeclaration();
      }
    }

    parseClassMember(
      classBody: N.ClassBody,
      member: any,
      state: N.ParseClassMemberState,
    ): void {
      const { startLoc } = this.state;
      if (this.isContextual(tt._declare)) {
        if (super.parseClassMemberFromModifier(classBody, member)) {
          // 'declare' is a class element name
          return;
        }

        member.declare = true;
      }

      super.parseClassMember(classBody, member, state);

      if (member.declare) {
        if (
          member.type !== "ClassProperty" &&
          member.type !== "ClassPrivateProperty" &&
          member.type !== "PropertyDefinition" // Used by estree plugin
        ) {
          this.raise(FlowErrors.DeclareClassElement, startLoc);
        } else if (member.value) {
          this.raise(FlowErrors.DeclareClassFieldInitializer, member.value);
        }
      }
    }

    isIterator(word: string): boolean {
      return word === "iterator" || word === "asyncIterator";
    }

    readIterator(): void {
      const word = super.readWord1();
      const fullWord = "@@" + word;

      // Allow @@iterator and @@asyncIterator as a identifier only inside type
      if (!this.isIterator(word) || !this.state.inType) {
        this.raise(Errors.InvalidIdentifier, this.state.curPosition(), {
          identifierName: fullWord,
        });
      }

      this.finishToken(tt.name, fullWord);
    }

    // ensure that inside flow types, we bypass the jsx parser plugin
    getTokenFromCode(code: number): void {
      const next = this.input.charCodeAt(this.state.pos + 1);
      if (code === charCodes.leftCurlyBrace && next === charCodes.verticalBar) {
        this.finishOp(tt.braceBarL, 2);
      } else if (
        this.state.inType &&
        (code === charCodes.greaterThan || code === charCodes.lessThan)
      ) {
        this.finishOp(code === charCodes.greaterThan ? tt.gt : tt.lt, 1);
      } else if (this.state.inType && code === charCodes.questionMark) {
        if (next === charCodes.dot) {
          this.finishOp(tt.questionDot, 2);
        } else {
          // allow double nullable types in Flow: ??string
          this.finishOp(tt.question, 1);
        }
      } else if (
        isIteratorStart(code, next, this.input.charCodeAt(this.state.pos + 2))
      ) {
        this.state.pos += 2; // eat "@@"
        this.readIterator();
      } else {
        super.getTokenFromCode(code);
      }
    }

    isAssignable(node: N.Node, isBinding?: boolean): boolean {
      if (node.type === "TypeCastExpression") {
        return this.isAssignable(node.expression, isBinding);
      } else {
        return super.isAssignable(node, isBinding);
      }
    }

    toAssignable(node: N.Node, isLHS: boolean = false): void {
      if (
        !isLHS &&
        node.type === "AssignmentExpression" &&
        // @ts-expect-error TypeCastExpression is not defined in AssignmentExpression.left
        node.left.type === "TypeCastExpression"
      ) {
        node.left = this.typeCastToParameter(node.left) as N.Assignable;
      }
      super.toAssignable(node, isLHS);
    }

    /**
     * turn type casts that we found in function parameter head into type annotated params
     */
    toAssignableListItem(
      exprList: (N.Expression | N.SpreadElement | N.RestElement)[],
      index: number,
      isLHS: boolean,
    ): void {
      const node = exprList[index];
      if (node.type === "TypeCastExpression") {
        exprList[index] = this.typeCastToParameter(node);
      }
      super.toAssignableListItem(exprList, index, isLHS);
    }

    // this is a list of nodes, from something like a call expression, we need to filter the
    // type casts that we've found that are illegal in this context
    toReferencedList(
      exprList:
        | readonly (
            | N.Expression
            | N.SpreadElement
            | N.VoidPattern
            | N.AssignmentPattern
            | N.ArgumentPlaceholder
            | N.TSTypeCastExpression
            | N.TypeCastExpression
            | null
          )[]
        | readonly (
            | N.Expression
            | N.RestElement
            | N.VoidPattern
            | N.AssignmentPattern
            | N.TSTypeCastExpression
            | N.TypeCastExpression
            | null
          )[],
      isParenthesizedExpr?: boolean,
    ):
      | readonly (
          | N.Expression
          | N.SpreadElement
          | N.VoidPattern
          | N.AssignmentPattern
          | N.ArgumentPlaceholder
          | N.TSTypeCastExpression
          | N.TypeCastExpression
          | null
        )[]
      | readonly (
          | N.Expression
          | N.RestElement
          | N.VoidPattern
          | N.AssignmentPattern
          | N.TSTypeCastExpression
          | N.TypeCastExpression
          | null
        )[] {
      for (let i = 0; i < exprList.length; i++) {
        const expr = exprList[i];
        if (
          expr?.type === "TypeCastExpression" &&
          !expr.extra?.parenthesized &&
          (exprList.length > 1 || !isParenthesizedExpr)
        ) {
          this.raise(FlowErrors.TypeCastInPattern, expr.typeAnnotation);
        }
      }

      return exprList;
    }

    parseArrayLike(
      close: TokenType,
      refExpressionErrors?: ExpressionErrors | null,
    ): N.ArrayExpression {
      const node = super.parseArrayLike(close, refExpressionErrors);

      // Check if there is any unparenthesized type cast
      if (node.type === "ArrayExpression") {
        this.toReferencedList(node.elements);
      }

      return node;
    }

    isValidLVal(
      type: string,
      disallowCallExpression: boolean,
      isParenthesized: boolean,
      binding: BindingFlag,
    ) {
      return (
        type === "TypeCastExpression" ||
        super.isValidLVal(
          type,
          disallowCallExpression,
          isParenthesized,
          binding,
        )
      );
    }

    // parse class property type annotations
    parseClassProperty(node: N.ClassProperty): N.ClassProperty {
      if (this.match(tt.colon)) {
        node.typeAnnotation = this.flowParseTypeAnnotation();
      }
      return super.parseClassProperty(node);
    }

    parseClassPrivateProperty(
      node: N.ClassPrivateProperty,
    ): N.ClassPrivateProperty {
      if (this.match(tt.colon)) {
        node.typeAnnotation = this.flowParseTypeAnnotation();
      }
      return super.parseClassPrivateProperty(node);
    }

    // determine whether or not we're currently in the position where a class method would appear
    isClassMethod(): boolean {
      return this.match(tt.lt) || super.isClassMethod();
    }

    // determine whether or not we're currently in the position where a class property would appear
    isClassProperty(): boolean {
      return this.match(tt.colon) || super.isClassProperty();
    }

    isNonstaticConstructor(method: N.ClassMethod | N.ClassProperty): boolean {
      return !this.match(tt.colon) && super.isNonstaticConstructor(method);
    }

    // parse type parameters for class methods
    pushClassMethod(
      classBody: Undone<N.ClassBody>,
      method: Undone<N.ClassMethod>,
      isGenerator: boolean,
      isAsync: boolean,
      isConstructor: boolean,
      allowsDirectSuper: boolean,
    ): void {
      if ((method as any).variance) {
        this.unexpected((method as any).variance.start);
      }
      delete (method as any).variance;
      if (this.match(tt.lt)) {
        method.typeParameters = this.flowParseTypeParameterDeclaration();
      }

      super.pushClassMethod(
        classBody,
        method,
        isGenerator,
        isAsync,
        isConstructor,
        allowsDirectSuper,
      );

      if (method.params && isConstructor) {
        const params = method.params;
        if (params.length > 0 && this.isThisParam(params[0])) {
          this.raise(FlowErrors.ThisParamBannedInConstructor, method);
        }
        // estree support
      } else if (
        // @ts-expect-error TS does not know about the fact that estree can replace ClassMethod with MethodDefinition
        method.type === "MethodDefinition" &&
        isConstructor &&
        // @ts-expect-error estree
        method.value.params
      ) {
        // @ts-expect-error estree
        const params = method.value.params;
        if (params.length > 0 && this.isThisParam(params[0])) {
          this.raise(FlowErrors.ThisParamBannedInConstructor, method);
        }
      }
    }

    pushClassPrivateMethod(
      classBody: Undone<N.ClassBody>,
      method: Undone<N.ClassPrivateMethod>,
      isGenerator: boolean,
      isAsync: boolean,
    ): void {
      if ((method as any).variance) {
        this.unexpected((method as any).variance.start);
      }
      delete (method as any).variance;
      if (this.match(tt.lt)) {
        method.typeParameters = this.flowParseTypeParameterDeclaration();
      }

      super.pushClassPrivateMethod(classBody, method, isGenerator, isAsync);
    }

    flowParseClassImplements() {
      const node = this.startNode<N.ClassImplements>();
      node.id = this.flowParseRestrictedIdentifier(/*liberal*/ true);
      if (this.match(tt.lt)) {
        node.typeParameters = this.flowParseTypeParameterInstantiation();
      } else {
        node.typeParameters = null;
      }
      return this.finishNode(node, "ClassImplements");
    }

    // parse a the super class type parameters and implements
    parseClassSuper(node: Undone<N.Class>): void {
      super.parseClassSuper(node);
      if (
        node.superClass &&
        (this.match(tt.lt) ||
          // handles `class extends C<<T>`
          this.match(tt.bitShiftL))
      ) {
        node.superTypeArguments =
          this.flowParseTypeParameterInstantiationInExpression();
      }

      if (this.eatContextual(tt._implements)) {
        const implemented: N.ClassImplements[] = (node.implements = []);
        do {
          implemented.push(this.flowParseClassImplements());
        } while (this.eat(tt.comma));
      }
    }

    checkGetterSetterParams(
      method: Undone<N.ObjectMethod | N.ClassMethod>,
    ): void {
      super.checkGetterSetterParams(method);
      const params = this.getObjectOrClassMethodParams(method);
      if (params.length > 0) {
        const param = params[0];
        if (this.isThisParam(param) && method.kind === "get") {
          this.raise(FlowErrors.GetterMayNotHaveThisParam, param);
        } else if (this.isThisParam(param)) {
          this.raise(FlowErrors.SetterMayNotHaveThisParam, param);
        }
      }
    }

    parsePropertyNamePrefixOperator(
      node: Undone<N.ObjectOrClassMember | N.ClassMember>,
    ): void {
      // @ts-expect-error: variance is not defined on ClassPrivateMethod
      node.variance = this.flowParseVariance();
    }

    // parse type parameters for object method shorthand
    parseObjPropValue<T extends N.ObjectMember>(
      prop: Undone<T>,
      startLoc: Position | undefined | null,
      isGenerator: boolean,
      isAsync: boolean,
      isPattern: boolean,
      isAccessor: boolean,
      refExpressionErrors?: ExpressionErrors | null,
    ): T {
      if ((prop as any).variance) {
        this.unexpected((prop as any).variance.start);
      }
      delete (prop as any).variance;

      let typeParameters;

      // method shorthand
      if (this.match(tt.lt) && !isAccessor) {
        typeParameters = this.flowParseTypeParameterDeclaration();
        if (!this.match(tt.parenL)) this.unexpected();
      }

      const result = super.parseObjPropValue(
        prop,
        startLoc,
        isGenerator,
        isAsync,
        isPattern,
        isAccessor,
        refExpressionErrors,
      );

      // add typeParameters if we found them
      if (typeParameters) {
        // @ts-expect-error: refine typings
        (result.value || result).typeParameters = typeParameters;
      }
      return result;
    }

    parseFunctionParamType<T extends N.Pattern | N.Identifier | N.RestElement>(
      param: T,
    ): T {
      if (this.eat(tt.question)) {
        if (param.type !== "Identifier") {
          this.raise(FlowErrors.PatternIsOptional, param);
        }
        if (this.isThisParam(param)) {
          this.raise(FlowErrors.ThisParamMayNotBeOptional, param);
        }

        (param as any as N.Identifier).optional = true;
      }
      if (this.match(tt.colon)) {
        // @ts-expect-error: typeAnnotation is not defined on VoidPattern
        param.typeAnnotation = this.flowParseTypeAnnotation();
      } else if (this.isThisParam(param)) {
        this.raise(FlowErrors.ThisParamAnnotationRequired, param);
      }

      if (this.match(tt.eq) && this.isThisParam(param)) {
        this.raise(FlowErrors.ThisParamNoDefault, param);
      }

      this.resetEndLocation(param);
      return param;
    }

    parseMaybeDefault<P extends N.Pattern | N.Identifier>(
      startLoc?: Position | null,
      left?: P | null,
    ): P | N.AssignmentPattern {
      const node = super.parseMaybeDefault(startLoc, left);

      if (
        node.type === "AssignmentPattern" &&
        node.typeAnnotation &&
        node.right.start! < node.typeAnnotation.start!
      ) {
        this.raise(FlowErrors.TypeBeforeInitializer, node.typeAnnotation);
      }

      return node;
    }

    parseImportSpecifierLocal<
      T extends
        | N.ImportSpecifier
        | N.ImportDefaultSpecifier
        | N.ImportNamespaceSpecifier,
    >(node: N.ImportDeclaration, specifier: Undone<T>, type: T["type"]): void {
      specifier.local = hasTypeImportKind(node)
        ? this.flowParseRestrictedIdentifier(
            /* liberal */ true,
            /* declaration */ true,
          )
        : this.parseIdentifier();

      node.specifiers.push(this.finishImportSpecifier(specifier, type));
    }

    isPotentialImportPhase(isExport: boolean): boolean {
      if (super.isPotentialImportPhase(isExport)) return true;
      if (this.isContextual(tt._type)) {
        if (!isExport) return true;
        const ch = this.lookaheadCharCode();
        return ch === charCodes.leftCurlyBrace || ch === charCodes.asterisk;
      }
      return !isExport && this.isContextual(tt._typeof);
    }

    applyImportPhase(
      node: Undone<N.ImportDeclaration | N.ExportNamedDeclaration>,
      isExport: boolean,
      phase: string | null,
      loc?: number,
    ): void {
      super.applyImportPhase(node, isExport, phase, loc);
      if (isExport) {
        if (!phase && this.match(tt._default)) {
          // TODO: Align with our TS AST and always add .exportKind
          return;
        }
        (node as N.ExportNamedDeclaration).exportKind =
          phase === "type" ? phase : "value";
      } else {
        if (phase === "type" && this.match(tt.star)) this.unexpected();
        (node as N.ImportDeclaration).importKind =
          phase === "type" || phase === "typeof" ? phase : "value";
      }
    }

    // parse import-type/typeof shorthand
    parseImportSpecifier(
      specifier: any,
      importedIsString: boolean,
      isInTypeOnlyImport: boolean,
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      isMaybeTypeOnly: boolean,
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      bindingType: BindingFlag | undefined,
    ): N.ImportSpecifier {
      const firstIdent = specifier.imported;

      let specifierTypeKind = null;
      if (firstIdent.type === "Identifier") {
        if (firstIdent.name === "type") {
          specifierTypeKind = "type";
        } else if (firstIdent.name === "typeof") {
          specifierTypeKind = "typeof";
        }
      }

      let isBinding = false;
      if (this.isContextual(tt._as) && !this.isLookaheadContextual("as")) {
        const as_ident = this.parseIdentifier(true);
        if (
          specifierTypeKind !== null &&
          !tokenIsKeywordOrIdentifier(this.state.type)
        ) {
          // `import {type as ,` or `import {type as }`
          specifier.imported = as_ident;
          specifier.importKind = specifierTypeKind;
          specifier.local = this.cloneIdentifier(as_ident);
        } else {
          // `import {type as foo`
          specifier.imported = firstIdent;
          specifier.importKind = null;
          specifier.local = this.parseIdentifier();
        }
      } else {
        if (
          specifierTypeKind !== null &&
          tokenIsKeywordOrIdentifier(this.state.type)
        ) {
          // `import {type foo`
          specifier.imported = this.parseIdentifier(true);
          specifier.importKind = specifierTypeKind;
        } else {
          if (importedIsString) {
            /*:: invariant(firstIdent instanceof N.StringLiteral) */
            throw this.raise(Errors.ImportBindingIsString, specifier, {
              importName: firstIdent.value,
            });
          }
          /*:: invariant(firstIdent instanceof N.Node) */
          specifier.imported = firstIdent;
          specifier.importKind = null;
        }

        if (this.eatContextual(tt._as)) {
          specifier.local = this.parseIdentifier();
        } else {
          isBinding = true;
          specifier.local = this.cloneIdentifier(specifier.imported);
        }
      }

      const specifierIsTypeImport = hasTypeImportKind(specifier);

      if (isInTypeOnlyImport && specifierIsTypeImport) {
        this.raise(FlowErrors.ImportTypeShorthandOnlyInPureImport, specifier);
      }

      if (isInTypeOnlyImport || specifierIsTypeImport) {
        this.checkReservedType(
          specifier.local.name,
          specifier.local.start,
          /* declaration */ true,
        );
      }

      if (isBinding && !isInTypeOnlyImport && !specifierIsTypeImport) {
        this.checkReservedWord(
          specifier.local.name,
          specifier.start,
          true,
          true,
        );
      }

      return this.finishImportSpecifier(specifier, "ImportSpecifier");
    }

    parseBindingAtom() {
      switch (this.state.type) {
        case tt._this:
          // "this" may be the name of a parameter, so allow it.
          return this.parseIdentifier(/* liberal */ true);
        default:
          return super.parseBindingAtom();
      }
    }

    // parse function type parameters - function foo<T>() {}
    parseFunctionParams(
      node: Undone<N.Function>,
      isConstructor: boolean,
    ): void {
      // @ts-expect-error kind may not index node
      const kind = node.kind;
      if (kind !== "get" && kind !== "set" && this.match(tt.lt)) {
        node.typeParameters = this.flowParseTypeParameterDeclaration();
      }
      super.parseFunctionParams(node, isConstructor);
    }

    // parse flow type annotations on variable declarator heads - let foo: string = bar
    parseVarId(
      decl: N.VariableDeclarator,
      kind: "var" | "let" | "const" | "using" | "await using",
    ): void {
      super.parseVarId(decl, kind);
      if (this.match(tt.colon)) {
        // @ts-expect-error typeAnnotation is not defined on VoidPattern
        decl.id.typeAnnotation = this.flowParseTypeAnnotation();
        this.resetEndLocation(decl.id); // set end position to end of type
      }
    }

    // parse the return type of an async arrow function - let foo = (async (): number => {});
    parseAsyncArrowFromCallExpression(
      node: N.ArrowFunctionExpression,
      call: N.CallExpression,
    ): N.ArrowFunctionExpression {
      if (this.match(tt.colon)) {
        const oldNoAnonFunctionType = this.state.noAnonFunctionType;
        this.state.noAnonFunctionType = true;
        node.returnType = this.flowParseTypeAnnotation();
        this.state.noAnonFunctionType = oldNoAnonFunctionType;
      }

      return super.parseAsyncArrowFromCallExpression(node, call);
    }

    // todo description
    shouldParseAsyncArrow(): boolean {
      return this.match(tt.colon) || super.shouldParseAsyncArrow();
    }

    // We need to support type parameter declarations for arrow functions. This
    // is tricky. There are three situations we need to handle
    //
    // 1. This is either JSX or an arrow function. We'll try JSX first. If that
    //    fails, we'll try an arrow function. If that fails, we'll throw the JSX
    //    error.
    // 2. This is an arrow function. We'll parse the type parameter declaration,
    //    parse the rest, make sure the rest is an arrow function, and go from
    //    there
    // 3. This is neither. Just call the super method
    parseMaybeAssign(
      refExpressionErrors?: ExpressionErrors | null,
      afterLeftParse?: Function,
    ): N.Expression {
      let state = null;

      let jsx;

      if (
        this.hasPlugin("jsx") &&
        (this.match(tt.jsxTagStart) || this.match(tt.lt))
      ) {
        state = this.state.clone();

        jsx = this.tryParse(
          () => super.parseMaybeAssign(refExpressionErrors, afterLeftParse),
          state,
        );

        /*:: invariant(!jsx.aborted) */
        /*:: invariant(jsx.node != null) */
        if (!jsx.error) return jsx.node!;

        // Remove `tc.j_expr` and `tc.j_oTag` from context added
        // by parsing `jsxTagStart` to stop the JSX plugin from
        // messing with the tokens
        const { context } = this.state;
        const currentContext = context[context.length - 1];
        if (currentContext === tc.j_oTag || currentContext === tc.j_expr) {
          context.pop();
        }
      }

      if (jsx?.error || this.match(tt.lt)) {
        state = state || this.state.clone();

        let typeParameters: N.TypeParameterDeclaration;

        const arrow = this.tryParse((abort: () => never) => {
          typeParameters = this.flowParseTypeParameterDeclaration();

          const arrowExpression = this.forwardNoArrowParamsConversionAt(
            typeParameters,
            () => {
              const result = super.parseMaybeAssign(
                refExpressionErrors,
                afterLeftParse,
              );

              this.resetStartLocationFromNode(result, typeParameters);

              return result;
            },
          );

          // <T>(() => {});
          // <T>(() => {}: any);
          if (arrowExpression.extra?.parenthesized) abort();

          // The above can return a TypeCastExpression when the arrow
          // expression is not wrapped in parens. See also `this.parseParenItem`.
          // (<T>() => {}: any);
          const expr = this.maybeUnwrapTypeCastExpression(arrowExpression);

          if (expr.type !== "ArrowFunctionExpression") abort();

          expr.typeParameters = typeParameters;
          this.resetStartLocationFromNode(expr, typeParameters);

          return arrowExpression;
        }, state);

        let arrowExpression:
          | N.ArrowFunctionExpression
          | N.TypeCastExpression
          | undefined
          | null = null;

        if (
          arrow.node &&
          this.maybeUnwrapTypeCastExpression(arrow.node).type ===
            "ArrowFunctionExpression"
        ) {
          if (!arrow.error && !arrow.aborted) {
            // <T> async () => {}
            // @ts-expect-error: refine tryParse typings
            if (arrow.node.async) {
              /*:: invariant(typeParameters) */
              this.raise(
                FlowErrors.UnexpectedTypeParameterBeforeAsyncArrowFunction,
                typeParameters!,
              );
            }
            return arrow.node;
          }

          // @ts-expect-error: refine typings
          arrowExpression = arrow.node;
        }

        // If we are here, both JSX and Flow parsing attempts failed.
        // Give the precedence to the JSX error, except if JSX had an
        // unrecoverable error while Flow didn't.
        // If the error is recoverable, we can only re-report it if there is
        // a node we can return.

        if (jsx?.node) {
          /*:: invariant(jsx.failState) */
          this.state = jsx.failState;
          return jsx.node;
        }

        if (arrowExpression) {
          /*:: invariant(arrow.failState) */
          this.state = arrow.failState!;
          return arrowExpression;
        }

        if (jsx?.thrown) throw jsx.error;
        if (arrow.thrown) throw arrow.error;

        /*:: invariant(typeParameters) */
        throw this.raise(
          FlowErrors.UnexpectedTokenAfterTypeParameter,
          typeParameters!,
        );
      }

      return super.parseMaybeAssign(refExpressionErrors, afterLeftParse);
    }

    // handle return types for arrow functions
    parseArrow(
      node: Undone<N.ArrowFunctionExpression>,
    ): Undone<N.ArrowFunctionExpression> | undefined | null {
      if (this.match(tt.colon)) {
        // @ts-expect-error todo(flow->ts)
        const result = this.tryParse<Undone<N.TypeAnnotation>>(() => {
          const oldNoAnonFunctionType = this.state.noAnonFunctionType;
          this.state.noAnonFunctionType = true;

          const typeNode = this.startNode<N.TypeAnnotation>();

          // @ts-expect-error if typeAnnotation is null, we will not assign
          // the TypeAnnotation to returnType
          [typeNode.typeAnnotation, node.predicate] =
            this.flowParseTypeAndPredicateInitialiser(true);

          this.state.noAnonFunctionType = oldNoAnonFunctionType;

          if (this.canInsertSemicolon()) this.unexpected();
          if (!this.match(tt.arrow)) this.unexpected();

          return typeNode;
        });

        if (result.thrown) return null;

        if (result.error) this.state = result.failState;

        // assign after it is clear it is an arrow
        node.returnType = result.node!.typeAnnotation
          ? this.finishNode(result.node!, "TypeAnnotation")
          : null;
      }

      return super.parseArrow(node);
    }

    shouldParseArrow(params: N.Node[]): boolean {
      return this.match(tt.colon) || super.shouldParseArrow(params);
    }

    setArrowFunctionParameters(
      node: Undone<N.ArrowFunctionExpression>,
      params:
        | (N.Expression | N.SpreadElement)[]
        | (N.Expression | N.RestElement)[],
    ): void {
      if (
        this.state.noArrowParamsConversionAt.includes(
          this.offsetToSourcePos(node.start!),
        )
      ) {
        node.params = params as N.ArrowFunctionExpression["params"];
      } else {
        super.setArrowFunctionParameters(node, params);
      }
    }

    checkParams(
      node: N.Function,
      allowDuplicates: boolean,
      isArrowFunction?: boolean | null,
      strictModeChanged: boolean = true,
    ): void {
      if (
        isArrowFunction &&
        this.state.noArrowParamsConversionAt.includes(
          this.offsetToSourcePos(node.start!),
        )
      ) {
        return;
      }

      // ensure the `this` param is first, if it exists
      for (let i = 0; i < node.params.length; i++) {
        if (this.isThisParam(node.params[i]) && i > 0) {
          this.raise(FlowErrors.ThisParamMustBeFirst, node.params[i]);
        }
      }

      super.checkParams(
        node,
        allowDuplicates,
        isArrowFunction,
        strictModeChanged,
      );
    }

    parseParenAndDistinguishExpression(canStartArrow: boolean): N.Expression {
      return super.parseParenAndDistinguishExpression(
        canStartArrow &&
          !this.state.noArrowAt.includes(
            this.sourceToOffsetPos(this.state.start),
          ),
      );
    }

    parseSubscripts(
      base: N.Expression | N.Super | N.Import,
      startLoc: Position,
      noCalls?: false | null,
    ): N.Expression;
    parseSubscripts(
      base: N.Expression | N.Super | N.Import,
      startLoc: Position,
      noCalls: true,
    ): N.Expression | N.Super | N.Import;
    parseSubscripts(
      base: N.Expression | N.Super | N.Import,
      startLoc: Position,
      noCalls?: boolean | null,
    ): N.Expression | N.Super | N.Import;
    parseSubscripts(
      base: N.Expression | N.Super | N.Import,
      startLoc: Position,
      noCalls?: boolean | null,
    ) {
      if (
        base.type === "Identifier" &&
        base.name === "async" &&
        this.state.noArrowAt.includes(startLoc.index)
      ) {
        this.next();

        const node = this.startNodeAt<N.CallExpression>(startLoc);
        node.callee = base;
        node.arguments = super.parseCallExpressionArguments();
        base = this.finishNode(node, "CallExpression");
      } else if (
        base.type === "Identifier" &&
        base.name === "async" &&
        this.match(tt.lt)
      ) {
        const state = this.state.clone();
        const arrow = this.tryParse(
          abort => this.parseAsyncArrowWithTypeParameters(startLoc) || abort(),
          state,
        );

        /*:: invariant(arrow.node != null) */
        if (!arrow.error && !arrow.aborted) return arrow.node;

        const result = this.tryParse(
          () => super.parseSubscripts(base, startLoc, noCalls),
          state,
        );

        if (result.node && !result.error) return result.node;

        if (arrow.node) {
          this.state = arrow.failState;
          return arrow.node;
        }

        if (result.node) {
          this.state = result.failState!;
          return result.node;
        }

        throw arrow.error || result.error!;
      }

      return super.parseSubscripts(base, startLoc, noCalls);
    }

    parseSubscript(
      base: N.Expression,
      startLoc: Position,
      noCalls: boolean | undefined | null,
      subscriptState: N.ParseSubscriptState,
    ): N.Expression {
      if (this.match(tt.questionDot) && this.isLookaheadToken_lt()) {
        subscriptState.optionalChainMember = true;
        if (noCalls) {
          subscriptState.stop = true;
          return base;
        }
        this.next();
        const node = this.startNodeAt<N.OptionalCallExpression>(startLoc);
        node.callee = base;
        node.typeArguments =
          this.flowParseTypeParameterInstantiationInExpression();
        this.expect(tt.parenL);
        node.arguments = this.parseCallExpressionArguments();
        node.optional = true;
        return this.finishCallExpression(node, /* optional */ true);
      } else if (
        !noCalls &&
        this.shouldParseTypes() &&
        (this.match(tt.lt) ||
          // also handles `new C<<T>`
          this.match(tt.bitShiftL))
      ) {
        const node = this.startNodeAt<
          N.OptionalCallExpression | N.CallExpression
        >(startLoc);
        node.callee = base;

        const result = this.tryParse(() => {
          node.typeArguments =
            this.flowParseTypeParameterInstantiationCallOrNew();
          this.expect(tt.parenL);
          node.arguments = super.parseCallExpressionArguments();
          if (subscriptState.optionalChainMember) {
            (node as Undone<N.OptionalCallExpression>).optional = false;
          }
          return this.finishCallExpression(
            node,
            subscriptState.optionalChainMember,
          );
        });

        if (result.node) {
          if (result.error) this.state = result.failState;
          return result.node;
        }
      }

      return super.parseSubscript(base, startLoc, noCalls, subscriptState);
    }

    parseNewCallee(node: N.NewExpression): void {
      super.parseNewCallee(node);

      let targs = null;
      if (this.shouldParseTypes() && this.match(tt.lt)) {
        targs = this.tryParse(() =>
          this.flowParseTypeParameterInstantiationCallOrNew(),
        ).node;
      }
      node.typeArguments = targs;
    }

    parseAsyncArrowWithTypeParameters(
      startLoc: Position,
    ): N.ArrowFunctionExpression | undefined | null {
      const node = this.startNodeAt<N.ArrowFunctionExpression>(startLoc);
      this.parseFunctionParams(node, false);
      if (!this.parseArrow(node)) return;
      return super.parseArrowExpression(
        node,
        /* params */ undefined,
        /* isAsync */ true,
      );
    }

    readToken_mult_modulo(code: number): void {
      const next = this.input.charCodeAt(this.state.pos + 1);
      if (
        code === charCodes.asterisk &&
        next === charCodes.slash &&
        this.state.hasFlowComment
      ) {
        this.state.hasFlowComment = false;
        this.state.pos += 2;
        this.nextToken();
        return;
      }

      super.readToken_mult_modulo(code);
    }

    readToken_pipe_amp(code: number): void {
      const next = this.input.charCodeAt(this.state.pos + 1);
      if (
        code === charCodes.verticalBar &&
        next === charCodes.rightCurlyBrace
      ) {
        // '|}'
        this.finishOp(tt.braceBarR, 2);
        return;
      }

      super.readToken_pipe_amp(code);
    }

    parseTopLevel(file: N.File, program: N.Program): N.File {
      const fileNode = super.parseTopLevel(file, program);
      if (this.state.hasFlowComment) {
        this.raise(
          FlowErrors.UnterminatedFlowComment,
          this.state.curPosition(),
        );
      }
      return fileNode;
    }

    skipBlockComment(): N.CommentBlock | undefined {
      if (this.hasPlugin("flowComments") && this.skipFlowComment()) {
        if (this.state.hasFlowComment) {
          throw this.raise(FlowErrors.NestedFlowComment, this.state.startLoc);
        }
        this.hasFlowCommentCompletion();
        const commentSkip = this.skipFlowComment();
        if (commentSkip) {
          this.state.pos += commentSkip;
          this.state.hasFlowComment = true;
        }
        return;
      }

      return super.skipBlockComment(this.state.hasFlowComment ? "*-/" : "*/");
    }

    skipFlowComment(): number | false {
      const { pos } = this.state;
      let shiftToFirstNonWhiteSpace = 2;
      while (
        [charCodes.space, charCodes.tab].includes(
          // @ts-expect-error testing whether a number is included
          this.input.charCodeAt(pos + shiftToFirstNonWhiteSpace),
        )
      ) {
        shiftToFirstNonWhiteSpace++;
      }

      const ch2 = this.input.charCodeAt(shiftToFirstNonWhiteSpace + pos);
      const ch3 = this.input.charCodeAt(shiftToFirstNonWhiteSpace + pos + 1);

      if (ch2 === charCodes.colon && ch3 === charCodes.colon) {
        return shiftToFirstNonWhiteSpace + 2; // check for /*::
      }
      if (
        this.input.slice(
          shiftToFirstNonWhiteSpace + pos,
          shiftToFirstNonWhiteSpace + pos + 12,
        ) === "flow-include"
      ) {
        return shiftToFirstNonWhiteSpace + 12; // check for /*flow-include
      }
      if (ch2 === charCodes.colon && ch3 !== charCodes.colon) {
        return shiftToFirstNonWhiteSpace; // check for /*:, advance up to :
      }
      return false;
    }

    hasFlowCommentCompletion(): void {
      const end = this.input.indexOf("*/", this.state.pos);
      if (end === -1) {
        throw this.raise(Errors.UnterminatedComment, this.state.curPosition());
      }
    }

    // Flow enum parsing

    flowEnumErrorBooleanMemberNotInitialized(
      loc: Position | number,
      names: {
        enumName: string;
        memberName: string;
      },
    ): void {
      this.raise(FlowErrors.EnumBooleanMemberNotInitialized, loc, names);
    }

    flowEnumErrorInvalidMemberInitializer(
      loc: Position | number,
      enumContext: EnumContext,
    ) {
      return this.raise(
        !enumContext.explicitType
          ? FlowErrors.EnumInvalidMemberInitializerUnknownType
          : enumContext.explicitType === "symbol"
            ? FlowErrors.EnumInvalidMemberInitializerSymbolType
            : FlowErrors.EnumInvalidMemberInitializerPrimaryType,
        loc,
        enumContext,
      );
    }

    flowEnumErrorNumberMemberNotInitialized(
      loc: Position | number,
      details: {
        enumName: string;
        memberName: string;
      },
    ): void {
      this.raise(FlowErrors.EnumNumberMemberNotInitialized, loc, details);
    }

    flowEnumErrorStringMemberInconsistentlyInitialized(
      node: N.Node,
      details: {
        enumName: string;
      },
    ): void {
      this.raise(
        FlowErrors.EnumStringMemberInconsistentlyInitialized,
        node,
        details,
      );
    }

    flowEnumMemberInit(): EnumMemberInit {
      const startLoc = this.state.startLoc;
      const endOfInit = () => this.match(tt.comma) || this.match(tt.braceR);
      switch (this.state.type) {
        case tt.num: {
          const literal = this.parseNumericLiteral(this.state.value);
          if (endOfInit()) {
            return { type: "number", loc: literal.start!, value: literal };
          }
          break;
        }
        case tt.string: {
          const literal = this.parseStringLiteral(this.state.value);
          if (endOfInit()) {
            return { type: "string", loc: literal.start!, value: literal };
          }
          break;
        }
        case tt._true:
        case tt._false: {
          const literal = this.parseBooleanLiteral(this.match(tt._true));
          if (endOfInit()) {
            return {
              type: "boolean",
              loc: literal.start!,
              value: literal,
            };
          }
        }
      }
      return { type: "invalid", loc: startLoc };
    }

    flowEnumMemberRaw(): {
      id: N.Identifier;
      init: EnumMemberInit;
    } {
      const loc = this.state.startLoc;
      const id = this.parseIdentifier(true);
      const init = this.eat(tt.eq)
        ? this.flowEnumMemberInit()
        : { type: "none" as const, loc };
      return { id, init };
    }

    flowEnumCheckExplicitTypeMismatch(
      loc: number,
      context: EnumContext,
      expectedType: EnumExplicitType,
    ): void {
      const { explicitType } = context;
      if (explicitType === null) {
        return;
      }
      if (explicitType !== expectedType) {
        this.flowEnumErrorInvalidMemberInitializer(loc, context);
      }
    }

    flowEnumMembers({
      enumName,
      explicitType,
    }: {
      enumName: string;
      explicitType: EnumExplicitType;
    }) {
      const seenNames = new Set();
      const members: {
        booleanMembers: N.EnumBooleanMember[];
        numberMembers: N.EnumNumberMember[];
        stringMembers: N.EnumStringMember[];
        defaultedMembers: N.EnumDefaultedMember[];
      } = {
        booleanMembers: [],
        numberMembers: [],
        stringMembers: [],
        defaultedMembers: [],
      };
      let hasUnknownMembers = false;
      while (!this.match(tt.braceR)) {
        if (this.eat(tt.ellipsis)) {
          hasUnknownMembers = true;
          break;
        }
        const memberNode = this.startNode<N.EnumMember>();
        const { id, init } = this.flowEnumMemberRaw();
        const memberName = id.name;
        if (memberName === "") {
          continue;
        }
        if (/^[a-z]/.test(memberName)) {
          this.raise(FlowErrors.EnumInvalidMemberName, id, {
            memberName,
            suggestion: memberName[0].toUpperCase() + memberName.slice(1),
            enumName,
          });
        }
        if (seenNames.has(memberName)) {
          this.raise(FlowErrors.EnumDuplicateMemberName, id, {
            memberName,
            enumName,
          });
        }
        seenNames.add(memberName);
        const context = { enumName, explicitType, memberName };
        memberNode.id = id;
        switch (init.type) {
          case "boolean": {
            this.flowEnumCheckExplicitTypeMismatch(
              init.loc,
              context,
              "boolean",
            );
            (memberNode as Undone<N.EnumBooleanMember>).init = init.value;
            members.booleanMembers.push(
              this.finishNode(
                memberNode as Undone<N.EnumBooleanMember>,
                "EnumBooleanMember",
              ),
            );
            break;
          }
          case "number": {
            this.flowEnumCheckExplicitTypeMismatch(init.loc, context, "number");
            (memberNode as Undone<N.EnumNumberMember>).init = init.value;
            members.numberMembers.push(
              this.finishNode(
                memberNode as Undone<N.EnumNumberMember>,
                "EnumNumberMember",
              ),
            );
            break;
          }
          case "string": {
            this.flowEnumCheckExplicitTypeMismatch(init.loc, context, "string");
            (memberNode as Undone<N.EnumStringMember>).init = init.value;
            members.stringMembers.push(
              this.finishNode(
                memberNode as Undone<N.EnumStringMember>,
                "EnumStringMember",
              ),
            );
            break;
          }
          case "invalid": {
            throw this.flowEnumErrorInvalidMemberInitializer(init.loc, context);
          }
          case "none": {
            switch (explicitType) {
              case "boolean":
                this.flowEnumErrorBooleanMemberNotInitialized(
                  init.loc,
                  context,
                );
                break;
              case "number":
                this.flowEnumErrorNumberMemberNotInitialized(init.loc, context);
                break;
              default:
                members.defaultedMembers.push(
                  this.finishNode(
                    // Without the type assertion, TS will throw
                    // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
                    memberNode as Undone<N.EnumDefaultedMember>,
                    "EnumDefaultedMember",
                  ),
                );
            }
          }
        }

        if (!this.match(tt.braceR)) {
          this.expect(tt.comma);
        }
      }
      return { members, hasUnknownMembers };
    }

    flowEnumStringMembers(
      initializedMembers: N.EnumStringMember[],
      defaultedMembers: N.EnumDefaultedMember[],
      {
        enumName,
      }: {
        enumName: string;
      },
    ) {
      if (initializedMembers.length === 0) {
        return defaultedMembers;
      } else if (defaultedMembers.length === 0) {
        return initializedMembers;
      } else if (defaultedMembers.length > initializedMembers.length) {
        for (const member of initializedMembers) {
          this.flowEnumErrorStringMemberInconsistentlyInitialized(member, {
            enumName,
          });
        }
        return defaultedMembers;
      } else {
        for (const member of defaultedMembers) {
          this.flowEnumErrorStringMemberInconsistentlyInitialized(member, {
            enumName,
          });
        }
        return initializedMembers;
      }
    }

    flowEnumParseExplicitType({
      enumName,
    }: {
      enumName: string;
    }): EnumExplicitType {
      if (!this.eatContextual(tt._of)) return null;

      if (!tokenIsIdentifier(this.state.type)) {
        throw this.raise(
          FlowErrors.EnumInvalidExplicitTypeUnknownSupplied,
          this.state.startLoc,
          {
            enumName,
          },
        );
      }

      const { value } = this.state;
      this.next();

      if (
        value !== "boolean" &&
        value !== "number" &&
        value !== "string" &&
        value !== "symbol"
      ) {
        this.raise(FlowErrors.EnumInvalidExplicitType, this.state.startLoc, {
          enumName,
          invalidEnumType: value,
        });
      }

      return value;
    }

    flowEnumBody(node: Undone<N.EnumBody>, id: N.Identifier) {
      const enumName = id.name;
      const nameLoc = id.start!;
      const explicitType = this.flowEnumParseExplicitType({ enumName });
      this.expect(tt.braceL);
      const { members, hasUnknownMembers } = this.flowEnumMembers({
        enumName,
        explicitType,
      });
      node.hasUnknownMembers = hasUnknownMembers;

      switch (explicitType) {
        case "boolean":
          (node as Undone<N.EnumBooleanBody>).explicitType = true;
          node.members = members.booleanMembers;
          this.expect(tt.braceR);
          return this.finishNode(node, "EnumBooleanBody");
        case "number":
          (node as Undone<N.EnumNumberBody>).explicitType = true;
          node.members = members.numberMembers;
          this.expect(tt.braceR);
          return this.finishNode(node, "EnumNumberBody");
        case "string":
          (node as Undone<N.EnumStringBody>).explicitType = true;
          (node as Undone<N.EnumStringBody>).members =
            this.flowEnumStringMembers(
              members.stringMembers,
              members.defaultedMembers,
              { enumName },
            );
          this.expect(tt.braceR);
          return this.finishNode(node, "EnumStringBody");
        case "symbol":
          node.members = members.defaultedMembers;
          this.expect(tt.braceR);
          return this.finishNode(node, "EnumSymbolBody");
        default: {
          // `explicitType` is `null`
          const empty = () => {
            node.members = [];
            this.expect(tt.braceR);
            return this.finishNode(node, "EnumStringBody");
          };
          (
            node as Undone<
              N.EnumStringBody | N.EnumBooleanBody | N.EnumNumberBody
            >
          ).explicitType = false;

          const boolsLen = members.booleanMembers.length;
          const numsLen = members.numberMembers.length;
          const strsLen = members.stringMembers.length;
          const defaultedLen = members.defaultedMembers.length;

          if (!boolsLen && !numsLen && !strsLen && !defaultedLen) {
            return empty();
          } else if (!boolsLen && !numsLen) {
            node.members = this.flowEnumStringMembers(
              members.stringMembers,
              members.defaultedMembers,
              { enumName },
            );
            this.expect(tt.braceR);
            return this.finishNode(node, "EnumStringBody");
          } else if (!numsLen && !strsLen && boolsLen >= defaultedLen) {
            for (const member of members.defaultedMembers) {
              this.flowEnumErrorBooleanMemberNotInitialized(member.start!, {
                enumName,
                memberName: member.id.name,
              });
            }
            node.members = members.booleanMembers;
            this.expect(tt.braceR);
            return this.finishNode(node, "EnumBooleanBody");
          } else if (!boolsLen && !strsLen && numsLen >= defaultedLen) {
            for (const member of members.defaultedMembers) {
              this.flowEnumErrorNumberMemberNotInitialized(member.start!, {
                enumName,
                memberName: member.id.name,
              });
            }
            node.members = members.numberMembers;
            this.expect(tt.braceR);
            return this.finishNode(node, "EnumNumberBody");
          } else {
            this.raise(FlowErrors.EnumInconsistentMemberValues, nameLoc, {
              enumName,
            });
            return empty();
          }
        }
      }
    }

    flowParseEnumDeclaration(
      node: Undone<N.EnumDeclaration>,
    ): N.EnumDeclaration {
      const id = this.parseIdentifier();
      node.id = id;
      node.body = this.flowEnumBody(this.startNode(), id);
      return this.finishNode(node, "EnumDeclaration");
    }

    jsxParseOpeningElementAfterName(
      node: N.JSXOpeningElement,
    ): N.JSXOpeningElement {
      if (this.shouldParseTypes()) {
        if (this.match(tt.lt) || this.match(tt.bitShiftL)) {
          node.typeArguments =
            this.flowParseTypeParameterInstantiationInExpression();
        }
      }

      return super.jsxParseOpeningElementAfterName(node);
    }

    // check if the next token is a tt.lt
    isLookaheadToken_lt(): boolean {
      const next = this.nextTokenStart();
      if (this.input.charCodeAt(next) === charCodes.lessThan) {
        const afterNext = this.input.charCodeAt(next + 1);
        return (
          afterNext !== charCodes.lessThan && afterNext !== charCodes.equalsTo
        );
      }
      return false;
    }

    // used after we have finished parsing types
    reScan_lt_gt() {
      const { type } = this.state;
      if (type === tt.lt) {
        this.state.pos -= 1;
        this.readToken_lt();
      } else if (type === tt.gt) {
        this.state.pos -= 1;
        this.readToken_gt();
      }
    }

    reScan_lt() {
      const { type } = this.state;
      if (type === tt.bitShiftL) {
        this.state.pos -= 2;
        this.finishOp(tt.lt, 1);
        return tt.lt;
      }
      return type;
    }

    maybeUnwrapTypeCastExpression(node: N.Node) {
      return node.type === "TypeCastExpression" ? node.expression : node;
    }
  };
