// @flow

/*:: declare var invariant; */

// Error messages are colocated with the plugin.
/* eslint-disable @babel/development-internal/dry-error-messages */

import type Parser from "../../parser";
import { types as tt, type TokenType } from "../../tokenizer/types";
import * as N from "../../types";
import type { Pos, Position } from "../../util/location";
import type State from "../../tokenizer/state";
import { types as tc } from "../../tokenizer/context";
import * as charCodes from "charcodes";
import { isIteratorStart, isKeyword } from "../../util/identifier";
import FlowScopeHandler from "./scope";
import {
  type BindingTypes,
  BIND_LEXICAL,
  BIND_VAR,
  BIND_FUNCTION,
  BIND_FLOW_DECLARE_FN,
  SCOPE_ARROW,
  SCOPE_FUNCTION,
  SCOPE_OTHER,
} from "../../util/scopeflags";
import type { ExpressionErrors } from "../../parser/util";
import { Errors } from "../../parser/error";

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
const FlowErrors = Object.freeze({
  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",
  AssignReservedType: "Cannot overwrite reserved type %0",
  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:
    "Boolean enum members need to be initialized. Use either `%0 = true,` or `%0 = false,` in enum `%1`.",
  EnumDuplicateMemberName:
    "Enum member names need to be unique, but the name `%0` has already been used before in enum `%1`.",
  EnumInconsistentMemberValues:
    "Enum `%0` has inconsistent member initializers. Either use no initializers, or consistently use literals (either booleans, numbers, or strings) for all member initializers.",
  EnumInvalidExplicitType:
    "Enum type `%1` is not valid. Use one of `boolean`, `number`, `string`, or `symbol` in enum `%0`.",
  EnumInvalidExplicitTypeUnknownSupplied:
    "Supplied enum type is not valid. Use one of `boolean`, `number`, `string`, or `symbol` in enum `%0`.",
  EnumInvalidMemberInitializerPrimaryType:
    "Enum `%0` has type `%2`, so the initializer of `%1` needs to be a %2 literal.",
  EnumInvalidMemberInitializerSymbolType:
    "Symbol enum members cannot be initialized. Use `%1,` in enum `%0`.",
  EnumInvalidMemberInitializerUnknownType:
    "The enum member initializer for `%1` needs to be a literal (either a boolean, number, or string) in enum `%0`.",
  EnumInvalidMemberName:
    "Enum member names cannot start with lowercase 'a' through 'z'. Instead of using `%0`, consider using `%1`, in enum `%2`.",
  EnumNumberMemberNotInitialized:
    "Number enum members need to be initialized, e.g. `%1 = 1` in enum `%0`.",
  EnumStringMemberInconsistentlyInitailized:
    "String enum members need to consistently either all use initializers, or use no initializers, in enum `%0`.",
  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",
  OptionalBindingPattern:
    "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: "Unexpected reserved type %0",
  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:
    "`declare export %0` is not supported. Use `%1` instead",
  UnsupportedStatementInDeclareModule:
    "Only declares and type imports are allowed inside declare module",
  UnterminatedFlowComment: "Unterminated flow-comment",
});
/* eslint-disable sort-keys */

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: N.Node): boolean {
  return node.importKind === "type" || node.importKind === "typeof";
}

function isMaybeDefaultImport(state: State): boolean {
  return (
    (state.type === tt.name || !!state.type.keyword) && state.value !== "from"
  );
}

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: (T, number, T[]) => ?boolean,
): [T[], T[]] {
  const list1 = [];
  const list2 = [];
  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", pos: number, value: N.Node |}
  | {| type: "string", pos: number, value: N.Node |}
  | {| type: "boolean", pos: number, value: N.Node |}
  | {| type: "invalid", pos: number |}
  | {| type: "none", pos: number |};

export default (superClass: Class<Parser>): Class<Parser> =>
  class extends superClass {
    // 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(): Class<FlowScopeHandler> {
      return FlowScopeHandler;
    }

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

    shouldParseEnums(): boolean {
      return !!this.getPluginOption("flow", "enums");
    }

    finishToken(type: TokenType, val: any): void {
      if (
        type !== tt.string &&
        type !== tt.semi &&
        type !== tt.interpreterDirective
      ) {
        if (this.flowPragma === undefined) {
          this.flowPragma = null;
        }
      }
      return 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");
        }
      }
      return 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.FlowType {
      const node = this.startNode();
      const moduloLoc = this.state.startLoc;
      const moduloPos = this.state.start;
      this.expect(tt.modulo);
      const checksLoc = this.state.startLoc;
      this.expectContextual("checks");
      // Force '%' and 'checks' to be adjacent
      if (
        moduloLoc.line !== checksLoc.line ||
        moduloLoc.column !== checksLoc.column - 1
      ) {
        this.raise(moduloPos, FlowErrors.UnexpectedSpaceBetweenModuloChecks);
      }
      if (this.eat(tt.parenL)) {
        node.value = this.parseExpression();
        this.expect(tt.parenR);
        return this.finishNode(node, "DeclaredPredicate");
      } else {
        return this.finishNode(node, "InferredPredicate");
      }
    }

    flowParseTypeAndPredicateInitialiser(): [?N.FlowType, ?N.FlowPredicate] {
      const oldInType = this.state.inType;
      this.state.inType = true;
      this.expect(tt.colon);
      let type = null;
      let predicate = null;
      if (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: N.FlowDeclareClass): N.FlowDeclareClass {
      this.next();
      this.flowParseInterfaceish(node, /*isClass*/ true);
      return this.finishNode(node, "DeclareClass");
    }

    flowParseDeclareFunction(
      node: N.FlowDeclareFunction,
    ): N.FlowDeclareFunction {
      this.next();

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

      const typeNode = this.startNode();
      const typeContainer = this.startNode();

      if (this.isRelational("<")) {
        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);

      [
        // $FlowFixMe (destructuring not supported yet)
        typeNode.returnType,
        // $FlowFixMe (destructuring not supported yet)
        node.predicate,
      ] = this.flowParseTypeAndPredicateInitialiser();

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

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

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

      this.scope.declareName(node.id.name, BIND_FLOW_DECLARE_FN, node.id.start);

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

    flowParseDeclare(
      node: N.FlowDeclare,
      insideModule?: boolean,
    ): N.FlowDeclare {
      if (this.match(tt._class)) {
        return this.flowParseDeclareClass(node);
      } else if (this.match(tt._function)) {
        return this.flowParseDeclareFunction(node);
      } else if (this.match(tt._var)) {
        return this.flowParseDeclareVariable(node);
      } else if (this.eatContextual("module")) {
        if (this.match(tt.dot)) {
          return this.flowParseDeclareModuleExports(node);
        } else {
          if (insideModule) {
            this.raise(this.state.lastTokStart, FlowErrors.NestedDeclareModule);
          }
          return this.flowParseDeclareModule(node);
        }
      } else if (this.isContextual("type")) {
        return this.flowParseDeclareTypeAlias(node);
      } else if (this.isContextual("opaque")) {
        return this.flowParseDeclareOpaqueType(node);
      } else if (this.isContextual("interface")) {
        return this.flowParseDeclareInterface(node);
      } else if (this.match(tt._export)) {
        return this.flowParseDeclareExportDeclaration(node, insideModule);
      } else {
        throw this.unexpected();
      }
    }

    flowParseDeclareVariable(
      node: N.FlowDeclareVariable,
    ): N.FlowDeclareVariable {
      this.next();
      node.id = this.flowParseTypeAnnotatableIdentifier(
        /*allowPrimitiveOverride*/ true,
      );
      this.scope.declareName(node.id.name, BIND_VAR, node.id.start);
      this.semicolon();
      return this.finishNode(node, "DeclareVariable");
    }

    flowParseDeclareModule(node: N.FlowDeclareModule): N.FlowDeclareModule {
      this.scope.enter(SCOPE_OTHER);

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

      const bodyNode = (node.body = this.startNode());
      const body = (bodyNode.body = []);
      this.expect(tt.braceL);
      while (!this.match(tt.braceR)) {
        let bodyNode = this.startNode();

        if (this.match(tt._import)) {
          this.next();
          if (!this.isContextual("type") && !this.match(tt._typeof)) {
            this.raise(
              this.state.lastTokStart,
              FlowErrors.InvalidNonTypeImportInDeclareModule,
            );
          }
          this.parseImport(bodyNode);
        } else {
          this.expectContextual(
            "declare",
            FlowErrors.UnsupportedStatementInDeclareModule,
          );

          bodyNode = this.flowParseDeclare(bodyNode, true);
        }

        body.push(bodyNode);
      }

      this.scope.exit();

      this.expect(tt.braceR);

      this.finishNode(bodyNode, "BlockStatement");

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

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

    flowParseDeclareExportDeclaration(
      node: N.FlowDeclareExportDeclaration,
      insideModule: ?boolean,
    ): N.FlowDeclareExportDeclaration {
      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("type") || this.isContextual("interface")) &&
            !insideModule)
        ) {
          const label = this.state.value;
          const suggestion = exportSuggestions[label];

          throw this.raise(
            this.state.start,
            FlowErrors.UnsupportedDeclareExportKind,
            label,
            suggestion,
          );
        }

        if (
          this.match(tt._var) || // declare export var ...
          this.match(tt._function) || // declare export function ...
          this.match(tt._class) || // declare export class ...
          this.isContextual("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("interface") || // declare export interface ...
          this.isContextual("type") || // declare export type ...
          this.isContextual("opaque") // declare export opaque type ...
        ) {
          node = this.parseExport(node);
          if (node.type === "ExportNamedDeclaration") {
            // flow does not support the ExportNamedDeclaration
            // $FlowIgnore
            node.type = "ExportDeclaration";
            // $FlowFixMe
            node.default = false;
            delete node.exportKind;
          }

          // $FlowIgnore
          node.type = "Declare" + node.type;

          return node;
        }
      }

      throw this.unexpected();
    }

    flowParseDeclareModuleExports(
      node: N.FlowDeclareModuleExports,
    ): N.FlowDeclareModuleExports {
      this.next();
      this.expectContextual("exports");
      node.typeAnnotation = this.flowParseTypeAnnotation();
      this.semicolon();

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

    flowParseDeclareTypeAlias(
      node: N.FlowDeclareTypeAlias,
    ): N.FlowDeclareTypeAlias {
      this.next();
      this.flowParseTypeAlias(node);
      // Don't do finishNode as we don't want to process comments twice
      node.type = "DeclareTypeAlias";
      return node;
    }

    flowParseDeclareOpaqueType(
      node: N.FlowDeclareOpaqueType,
    ): N.FlowDeclareOpaqueType {
      this.next();
      this.flowParseOpaqueType(node, true);
      // Don't do finishNode as we don't want to process comments twice
      node.type = "DeclareOpaqueType";
      return node;
    }

    flowParseDeclareInterface(
      node: N.FlowDeclareInterface,
    ): N.FlowDeclareInterface {
      this.next();
      this.flowParseInterfaceish(node);
      return this.finishNode(node, "DeclareInterface");
    }

    // Interfaces

    flowParseInterfaceish(
      node: N.FlowDeclare,
      isClass?: boolean = false,
    ): void {
      node.id = this.flowParseRestrictedIdentifier(
        /* liberal */ !isClass,
        /* declaration */ true,
      );

      this.scope.declareName(
        node.id.name,
        isClass ? BIND_FUNCTION : BIND_LEXICAL,
        node.id.start,
      );

      if (this.isRelational("<")) {
        node.typeParameters = this.flowParseTypeParameterDeclaration();
      } else {
        node.typeParameters = null;
      }

      node.extends = [];
      node.implements = [];
      node.mixins = [];

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

      if (this.isContextual("mixins")) {
        this.next();
        do {
          node.mixins.push(this.flowParseInterfaceExtends());
        } while (this.eat(tt.comma));
      }

      if (this.isContextual("implements")) {
        this.next();
        do {
          node.implements.push(this.flowParseInterfaceExtends());
        } while (this.eat(tt.comma));
      }

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

    flowParseInterfaceExtends(): N.FlowInterfaceExtends {
      const node = this.startNode();

      node.id = this.flowParseQualifiedTypeIdentifier();
      if (this.isRelational("<")) {
        node.typeParameters = this.flowParseTypeParameterInstantiation();
      } else {
        node.typeParameters = null;
      }

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

    flowParseInterface(node: N.FlowInterface): N.FlowInterface {
      this.flowParseInterfaceish(node);
      return this.finishNode(node, "InterfaceDeclaration");
    }

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

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

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

    flowParseRestrictedIdentifier(
      liberal?: boolean,
      declaration?: boolean,
    ): N.Identifier {
      this.checkReservedType(this.state.value, this.state.start, declaration);
      return this.parseIdentifier(liberal);
    }

    // Type aliases

    flowParseTypeAlias(node: N.FlowTypeAlias): N.FlowTypeAlias {
      node.id = this.flowParseRestrictedIdentifier(
        /* liberal */ false,
        /* declaration */ true,
      );
      this.scope.declareName(node.id.name, BIND_LEXICAL, node.id.start);

      if (this.isRelational("<")) {
        node.typeParameters = this.flowParseTypeParameterDeclaration();
      } else {
        node.typeParameters = null;
      }

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

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

    flowParseOpaqueType(
      node: N.FlowOpaqueType,
      declare: boolean,
    ): N.FlowOpaqueType {
      this.expectContextual("type");
      node.id = this.flowParseRestrictedIdentifier(
        /* liberal */ true,
        /* declaration */ true,
      );
      this.scope.declareName(node.id.name, BIND_LEXICAL, node.id.start);

      if (this.isRelational("<")) {
        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, "OpaqueType");
    }

    // Type annotations

    flowParseTypeParameter(requireDefault?: boolean = false): N.TypeParameter {
      const nodeStart = this.state.start;

      const node = this.startNode();

      const variance = this.flowParseVariance();

      const ident = this.flowParseTypeAnnotatableIdentifier();
      node.name = ident.name;
      node.variance = variance;
      node.bound = ident.typeAnnotation;

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

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

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

      this.state.inType = true;

      // istanbul ignore else: this condition is already checked at all call sites
      if (this.isRelational("<") || 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.isRelational(">")) {
          this.expect(tt.comma);
        }
      } while (!this.isRelational(">"));
      this.expectRelational(">");

      this.state.inType = oldInType;

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

    flowParseTypeParameterInstantiation(): N.TypeParameterInstantiation {
      const node = this.startNode();
      const oldInType = this.state.inType;
      node.params = [];

      this.state.inType = true;

      this.expectRelational("<");
      const oldNoAnonFunctionType = this.state.noAnonFunctionType;
      this.state.noAnonFunctionType = false;
      while (!this.isRelational(">")) {
        node.params.push(this.flowParseType());
        if (!this.isRelational(">")) {
          this.expect(tt.comma);
        }
      }
      this.state.noAnonFunctionType = oldNoAnonFunctionType;
      this.expectRelational(">");

      this.state.inType = oldInType;

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

    flowParseTypeParameterInstantiationCallOrNew(): N.TypeParameterInstantiation {
      const node = this.startNode();
      const oldInType = this.state.inType;
      node.params = [];

      this.state.inType = true;

      this.expectRelational("<");
      while (!this.isRelational(">")) {
        node.params.push(this.flowParseTypeOrImplicitInstantiation());
        if (!this.isRelational(">")) {
          this.expect(tt.comma);
        }
      }
      this.expectRelational(">");

      this.state.inType = oldInType;

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

    flowParseInterfaceType(): N.FlowInterfaceType {
      const node = this.startNode();
      this.expectContextual("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(): N.Expression {
      return this.match(tt.num) || this.match(tt.string)
        ? this.parseExprAtom()
        : this.parseIdentifier(true);
    }

    flowParseObjectTypeIndexer(
      node: N.FlowObjectTypeIndexer,
      isStatic: boolean,
      variance: ?N.FlowVariance,
    ): N.FlowObjectTypeIndexer {
      node.static = isStatic;

      // Note: bracketL has already been consumed
      if (this.lookahead().type === tt.colon) {
        node.id = this.flowParseObjectPropertyKey();
        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: N.FlowObjectTypeInternalSlot,
      isStatic: boolean,
    ): N.FlowObjectTypeInternalSlot {
      node.static = isStatic;
      // Note: both bracketL have already been consumed
      node.id = this.flowParseObjectPropertyKey();
      this.expect(tt.bracketR);
      this.expect(tt.bracketR);
      if (this.isRelational("<") || this.match(tt.parenL)) {
        node.method = true;
        node.optional = false;
        node.value = this.flowParseObjectTypeMethodish(
          this.startNodeAt(node.start, node.loc.start),
        );
      } else {
        node.method = false;
        if (this.eat(tt.question)) {
          node.optional = true;
        }
        node.value = this.flowParseTypeInitialiser();
      }
      return this.finishNode(node, "ObjectTypeInternalSlot");
    }

    flowParseObjectTypeMethodish(
      node: N.FlowFunctionTypeAnnotation,
    ): N.FlowFunctionTypeAnnotation {
      node.params = [];
      node.rest = null;
      node.typeParameters = null;
      node.this = null;

      if (this.isRelational("<")) {
        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: N.FlowObjectTypeCallProperty,
      isStatic: boolean,
    ): N.FlowObjectTypeCallProperty {
      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.FlowObjectTypeAnnotation {
      const oldInType = this.state.inType;
      this.state.inType = true;

      const nodeStart = this.startNode();

      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 protoStart: ?number = null;
        let inexactStart: ?number = null;
        const node = this.startNode();

        if (allowProto && this.isContextual("proto")) {
          const lookahead = this.lookahead();

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

        if (allowStatic && this.isContextual("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 (protoStart != null) {
            this.unexpected(protoStart);
          }
          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.isRelational("<")) {
          if (protoStart != null) {
            this.unexpected(protoStart);
          }
          if (variance) {
            this.unexpected(variance.start);
          }
          nodeStart.callProperties.push(
            this.flowParseObjectTypeCallProperty(node, isStatic),
          );
        } else {
          let kind = "init";

          if (this.isContextual("get") || this.isContextual("set")) {
            const lookahead = this.lookahead();
            if (
              lookahead.type === tt.name ||
              lookahead.type === tt.string ||
              lookahead.type === tt.num
            ) {
              kind = this.state.value;
              this.next();
            }
          }

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

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

        this.flowObjectTypeSemicolon();

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

      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: N.FlowObjectTypeProperty | N.FlowObjectTypeSpreadProperty,
      isStatic: boolean,
      protoStart: ?number,
      variance: ?N.FlowVariance,
      kind: string,
      allowSpread: boolean,
      allowInexact: boolean,
    ): (N.FlowObjectTypeProperty | N.FlowObjectTypeSpreadProperty) | 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(
              this.state.lastTokStart,
              FlowErrors.InexactInsideNonObject,
            );
          } else if (!allowInexact) {
            this.raise(this.state.lastTokStart, FlowErrors.InexactInsideExact);
          }
          if (variance) {
            this.raise(variance.start, FlowErrors.InexactVariance);
          }

          return null;
        }

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

        node.argument = this.flowParseType();
        return this.finishNode(node, "ObjectTypeSpreadProperty");
      } else {
        node.key = this.flowParseObjectPropertyKey();
        node.static = isStatic;
        node.proto = protoStart != null;
        node.kind = kind;

        let optional = false;
        if (this.isRelational("<") || this.match(tt.parenL)) {
          // This is a method property
          node.method = true;

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

          node.value = this.flowParseObjectTypeMethodish(
            this.startNodeAt(node.start, node.loc.start),
          );
          if (kind === "get" || kind === "set") {
            this.flowCheckGetterSetterParams(node);
          }
          /** Declared classes/interfaces do not allow spread */
          if (
            !allowSpread &&
            node.key.name === "constructor" &&
            node.value.this
          ) {
            this.raise(
              node.value.this.start,
              FlowErrors.ThisParamBannedInConstructor,
            );
          }
        } else {
          if (kind !== "init") this.unexpected();

          node.method = false;

          if (this.eat(tt.question)) {
            optional = true;
          }
          node.value = this.flowParseTypeInitialiser();
          node.variance = variance;
        }

        node.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: N.FlowObjectTypeProperty | N.FlowObjectTypeSpreadProperty,
    ): void {
      const paramCount = property.kind === "get" ? 0 : 1;
      const start = property.start;
      const length =
        property.value.params.length + (property.value.rest ? 1 : 0);

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

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

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

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

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

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

      return node;
    }

    flowParseGenericType(
      startPos: number,
      startLoc: Position,
      id: N.Identifier,
    ): N.FlowGenericTypeAnnotation {
      const node = this.startNodeAt(startPos, startLoc);

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

      if (this.isRelational("<")) {
        node.typeParameters = this.flowParseTypeParameterInstantiation();
      }

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

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

    flowParseTupleType(): N.FlowTupleTypeAnnotation {
      const node = this.startNode();
      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.FlowFunctionTypeParam {
      let name = null;
      let optional = false;
      let typeAnnotation = null;
      const node = this.startNode();
      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(node.start, FlowErrors.ThisParamMustBeFirst);
        }
        name = this.parseIdentifier(isThis);
        if (this.eat(tt.question)) {
          optional = true;
          if (isThis) {
            this.raise(node.start, FlowErrors.ThisParamMayNotBeOptional);
          }
        }
        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.FlowFunctionTypeParam {
      const node = this.startNodeAt(type.start, type.loc.start);
      node.name = null;
      node.optional = false;
      node.typeAnnotation = type;
      return this.finishNode(node, "FunctionTypeParam");
    }

    flowParseFunctionTypeParams(
      params: N.FlowFunctionTypeParam[] = [],
    ): {
      params: N.FlowFunctionTypeParam[],
      rest: ?N.FlowFunctionTypeParam,
      _this: ?N.FlowFunctionTypeParam,
    } {
      let rest: ?N.FlowFunctionTypeParam = null;
      let _this: ?N.FlowFunctionTypeParam = 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(
      startPos: number,
      startLoc: Position,
      node: N.FlowTypeAnnotation,
      id: N.Identifier,
    ): N.FlowTypeAnnotation {
      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(startPos, 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.FlowTypeAnnotation {
      const startPos = this.state.start;
      const startLoc = this.state.startLoc;
      const node = this.startNode();
      let tmp;
      let type;
      let isGroupedType = false;
      const oldNoAnonFunctionType = this.state.noAnonFunctionType;

      switch (this.state.type) {
        case tt.name:
          if (this.isContextual("interface")) {
            return this.flowParseInterfaceType();
          }

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

        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.relational:
          if (this.state.value === "<") {
            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");
          }
          break;

        case tt.parenL:
          this.next();

          // Check to see if this is actually a grouped type
          if (!this.match(tt.parenR) && !this.match(tt.ellipsis)) {
            if (this.match(tt.name) || 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(
            this.state.value,
            "StringLiteralTypeAnnotation",
          );

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

        case tt.plusMin:
          if (this.state.value === "-") {
            this.next();
            if (this.match(tt.num)) {
              return this.parseLiteral(
                -this.state.value,
                "NumberLiteralTypeAnnotation",
                node.start,
                node.loc.start,
              );
            }

            if (this.match(tt.bigint)) {
              return this.parseLiteral(
                -this.state.value,
                "BigIntLiteralTypeAnnotation",
                node.start,
                node.loc.start,
              );
            }

            throw this.raise(
              this.state.start,
              FlowErrors.UnexpectedSubtractionOperand,
            );
          }

          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");

        default:
          if (this.state.type.keyword === "typeof") {
            return this.flowParseTypeofType();
          } else if (this.state.type.keyword) {
            const label = this.state.type.label;
            this.next();
            return super.createIdentifier(node, label);
          }
      }

      throw this.unexpected();
    }

    flowParsePostfixType(): N.FlowTypeAnnotation {
      const startPos = this.state.start,
        startLoc = this.state.startLoc;
      let type = this.flowParsePrimaryType();
      while (this.match(tt.bracketL) && !this.canInsertSemicolon()) {
        const node = this.startNodeAt(startPos, startLoc);
        node.elementType = type;
        this.expect(tt.bracketL);
        this.expect(tt.bracketR);
        type = this.finishNode(node, "ArrayTypeAnnotation");
      }
      return type;
    }

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

    flowParseAnonFunctionWithoutParens(): N.FlowTypeAnnotation {
      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.startNodeAt(param.start, param.loc.start);
        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.FlowTypeAnnotation {
      const node = this.startNode();
      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.FlowTypeAnnotation {
      const node = this.startNode();
      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.FlowTypeAnnotation {
      const oldInType = this.state.inType;
      this.state.inType = true;
      const type = this.flowParseUnionType();
      this.state.inType = oldInType;
      // Ensure that a brace after a function generic type annotation is a
      // statement, except in arrow functions (noAnonFunctionType)
      this.state.exprAllowed =
        this.state.exprAllowed || this.state.noAnonFunctionType;
      return type;
    }

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

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

    flowParseTypeAnnotatableIdentifier(
      allowPrimitiveOverride?: boolean,
    ): N.Identifier {
      const ident = allowPrimitiveOverride
        ? this.parseIdentifier()
        : this.flowParseRestrictedIdentifier();
      if (this.match(tt.colon)) {
        ident.typeAnnotation = this.flowParseTypeAnnotation();
        this.resetEndLocation(ident);
      }
      return ident;
    }

    typeCastToParameter(node: N.Node): N.Node {
      node.expression.typeAnnotation = node.typeAnnotation;

      this.resetEndLocation(
        node.expression,
        node.typeAnnotation.end,
        node.typeAnnotation.loc.end,
      );

      return node.expression;
    }

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

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

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

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

    parseFunctionBodyAndFinish(
      node: N.BodilessFunctionOrMethodBase,
      type: string,
      isMethod?: boolean = false,
    ): void {
      if (this.match(tt.colon)) {
        const typeNode = this.startNode();

        [
          // $FlowFixMe (destructuring not supported yet)
          typeNode.typeAnnotation,
          // $FlowFixMe (destructuring not supported yet)
          node.predicate,
        ] = this.flowParseTypeAndPredicateInitialiser();

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

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

    // interfaces and enums
    parseStatement(context: ?string, topLevel?: boolean): N.Statement {
      // strict mode handling of `interface` since it's a reserved word
      if (
        this.state.strict &&
        this.match(tt.name) &&
        this.state.value === "interface"
      ) {
        const lookahead = this.lookahead();
        if (lookahead.type === tt.name || isKeyword(lookahead.value)) {
          const node = this.startNode();
          this.next();
          return this.flowParseInterface(node);
        }
      } else if (this.shouldParseEnums() && this.isContextual("enum")) {
        const node = this.startNode();
        this.next();
        return this.flowParseEnumDeclaration(node);
      }
      const stmt = super.parseStatement(context, topLevel);
      // 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: N.ExpressionStatement,
      expr: N.Expression,
    ): N.ExpressionStatement {
      if (expr.type === "Identifier") {
        if (expr.name === "declare") {
          if (
            this.match(tt._class) ||
            this.match(tt.name) ||
            this.match(tt._function) ||
            this.match(tt._var) ||
            this.match(tt._export)
          ) {
            return this.flowParseDeclare(node);
          }
        } else if (this.match(tt.name)) {
          if (expr.name === "interface") {
            return this.flowParseInterface(node);
          } else if (expr.name === "type") {
            return this.flowParseTypeAlias(node);
          } else if (expr.name === "opaque") {
            return this.flowParseOpaqueType(node, false);
          }
        }
      }

      return super.parseExpressionStatement(node, expr);
    }

    // export type
    shouldParseExportDeclaration(): boolean {
      return (
        this.isContextual("type") ||
        this.isContextual("interface") ||
        this.isContextual("opaque") ||
        (this.shouldParseEnums() && this.isContextual("enum")) ||
        super.shouldParseExportDeclaration()
      );
    }

    isExportDefaultSpecifier(): boolean {
      if (
        this.match(tt.name) &&
        (this.state.value === "type" ||
          this.state.value === "interface" ||
          this.state.value === "opaque" ||
          (this.shouldParseEnums() && this.state.value === "enum"))
      ) {
        return false;
      }

      return super.isExportDefaultSpecifier();
    }

    parseExportDefaultExpression(): N.Expression | N.Declaration {
      if (this.shouldParseEnums() && this.isContextual("enum")) {
        const node = this.startNode();
        this.next();
        return this.flowParseEnumDeclaration(node);
      }
      return super.parseExportDefaultExpression();
    }

    parseConditional(
      expr: N.Expression,
      startPos: number,
      startLoc: Position,
      refNeedsArrowPos?: ?Pos,
    ): N.Expression {
      if (!this.match(tt.question)) return expr;

      // only use the expensive "tryParse" method if there is a question mark
      // and if we come from inside parens
      if (refNeedsArrowPos) {
        const result = this.tryParse(() =>
          super.parseConditional(expr, startPos, startLoc),
        );

        if (!result.node) {
          // $FlowIgnore
          refNeedsArrowPos.start = result.error.pos || this.state.start;
          return expr;
        }

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

      this.expect(tt.question);
      const state = this.state.clone();
      const originalNoArrowAt = this.state.noArrowAt;
      const node = this.startNodeAt(startPos, startLoc);
      let { consequent, failed } = this.tryParseConditionalConsequent();
      let [valid, invalid] = this.getArrowLikeExpressions(consequent);

      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, invalid] = 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(state.start, FlowErrors.AmbiguousConditionalArrow);
        }

        if (failed && valid.length === 1) {
          this.state = state;
          this.state.noArrowAt = noArrowAt.concat(valid[0].start);
          ({ consequent, failed } = 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, 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") {
          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: any): N.Expression[]),
        node.extra?.trailingComma,
        /* isLHS */ false,
      );
      // Enter scope, as checkParams defines bindings
      this.scope.enter(SCOPE_FUNCTION | SCOPE_ARROW);
      // Use super's method to force the parameters to be checked
      super.checkParams(node, false, true);
      this.scope.exit();
    }

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

      return result;
    }

    parseParenItem(
      node: N.Expression,
      startPos: number,
      startLoc: Position,
    ): N.Expression {
      node = super.parseParenItem(node, startPos, startLoc);
      if (this.eat(tt.question)) {
        node.optional = true;
        // Include questionmark 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(startPos, startLoc);
        typeCastNode.expression = node;
        typeCastNode.typeAnnotation = this.flowParseTypeAnnotation();

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

      return node;
    }

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

    parseExport(node: N.Node): N.AnyExport {
      const decl = super.parseExport(node);
      if (
        decl.type === "ExportNamedDeclaration" ||
        decl.type === "ExportAllDeclaration"
      ) {
        decl.exportKind = decl.exportKind || "value";
      }
      return decl;
    }

    parseExportDeclaration(node: N.ExportNamedDeclaration): ?N.Declaration {
      if (this.isContextual("type")) {
        node.exportKind = "type";

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

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

        const declarationNode = this.startNode();
        this.next();
        // export opaque type Foo = Bar;
        return this.flowParseOpaqueType(declarationNode, false);
      } else if (this.isContextual("interface")) {
        node.exportKind = "type";
        const declarationNode = this.startNode();
        this.next();
        return this.flowParseInterface(declarationNode);
      } else if (this.shouldParseEnums() && this.isContextual("enum")) {
        node.exportKind = "value";
        const declarationNode = this.startNode();
        this.next();
        return this.flowParseEnumDeclaration(declarationNode);
      } else {
        return super.parseExportDeclaration(node);
      }
    }

    eatExportStar(node: N.Node): boolean {
      if (super.eatExportStar(...arguments)) return true;

      if (this.isContextual("type") && this.lookahead().type === tt.star) {
        node.exportKind = "type";
        this.next();
        this.next();
        return true;
      }

      return false;
    }

    maybeParseExportNamespaceSpecifier(node: N.Node): boolean {
      const pos = this.state.start;
      const hasNamespace = super.maybeParseExportNamespaceSpecifier(node);
      if (hasNamespace && node.exportKind === "type") {
        this.unexpected(pos);
      }
      return hasNamespace;
    }

    parseClassId(node: N.Class, isStatement: boolean, optionalId: ?boolean) {
      super.parseClassId(node, isStatement, optionalId);
      if (this.isRelational("<")) {
        node.typeParameters = this.flowParseTypeParameterDeclaration();
      }
    }

    parseClassMember(
      classBody: N.ClassBody,
      member: any,
      state: N.ParseClassMemberState,
    ): void {
      const pos = this.state.start;
      if (this.isContextual("declare")) {
        if (this.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(pos, FlowErrors.DeclareClassElement);
        } else if (member.value) {
          this.raise(
            member.value.start,
            FlowErrors.DeclareClassFieldInitializer,
          );
        }
      }
    }

    // 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) {
        return this.finishOp(tt.braceBarL, 2);
      } else if (
        this.state.inType &&
        (code === charCodes.greaterThan || code === charCodes.lessThan)
      ) {
        return this.finishOp(tt.relational, 1);
      } else if (this.state.inType && code === charCodes.questionMark) {
        // allow double nullable types in Flow: ??string
        return this.finishOp(tt.question, 1);
      } else if (isIteratorStart(code, next)) {
        this.state.isIterator = true;
        return super.readWord();
      } else {
        return super.getTokenFromCode(code);
      }
    }

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

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

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

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

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

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

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

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

        default:
          return false;
      }
    }

    toAssignable(node: N.Node, isLHS: boolean = false): N.Node {
      if (node.type === "TypeCastExpression") {
        return super.toAssignable(this.typeCastToParameter(node), isLHS);
      } else {
        return super.toAssignable(node, isLHS);
      }
    }

    // turn type casts that we found in function parameter head into type annotated params
    toAssignableList(
      exprList: N.Expression[],
      trailingCommaPos?: ?number,
      isLHS: boolean,
    ): $ReadOnlyArray<N.Pattern> {
      for (let i = 0; i < exprList.length; i++) {
        const expr = exprList[i];
        if (expr?.type === "TypeCastExpression") {
          exprList[i] = this.typeCastToParameter(expr);
        }
      }
      return super.toAssignableList(exprList, trailingCommaPos, 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: $ReadOnlyArray<?N.Expression>,
      isParenthesizedExpr?: boolean,
    ): $ReadOnlyArray<?N.Expression> {
      for (let i = 0; i < exprList.length; i++) {
        const expr = exprList[i];
        if (
          expr &&
          expr.type === "TypeCastExpression" &&
          !expr.extra?.parenthesized &&
          (exprList.length > 1 || !isParenthesizedExpr)
        ) {
          this.raise(expr.typeAnnotation.start, FlowErrors.TypeCastInPattern);
        }
      }

      return exprList;
    }

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

      // This could be an array pattern:
      //   ([a: string, b: string]) => {}
      // In this case, we don't have to call toReferencedList. We will
      // call it, if needed, when we are sure that it is a parenthesized
      // expression by calling toReferencedListDeep.
      if (canBePattern && !this.state.maybeInArrowParameters) {
        this.toReferencedList(node.elements);
      }

      return node;
    }

    checkLVal(
      expr: N.Expression,
      ...args:
        | [string, BindingTypes | void]
        | [
            string,
            BindingTypes | void,
            ?Set<string>,
            boolean | void,
            boolean | void,
          ]
    ): void {
      if (expr.type !== "TypeCastExpression") {
        return super.checkLVal(expr, ...args);
      }
    }

    // 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.isRelational("<") || 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);
    }

    // determine whether a parameter is a this param
    isThisParam(param) {
      return param.type === "Identifier" && param.name === "this";
    }

    // parse type parameters for class methods
    pushClassMethod(
      classBody: N.ClassBody,
      method: N.ClassMethod,
      isGenerator: boolean,
      isAsync: boolean,
      isConstructor: boolean,
      allowsDirectSuper: boolean,
    ): void {
      if ((method: $FlowFixMe).variance) {
        this.unexpected((method: $FlowFixMe).variance.start);
      }
      delete (method: $FlowFixMe).variance;
      if (this.isRelational("<")) {
        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(method.start, FlowErrors.ThisParamBannedInConstructor);
        }
        // estree support
      } else if (
        // $FlowFixMe flow does not know about the face that estree can replace ClassMethod with MethodDefinition
        method.type === "MethodDefinition" &&
        isConstructor &&
        method.value.params
      ) {
        const params = method.value.params;
        if (params.length > 0 && this.isThisParam(params[0])) {
          this.raise(method.start, FlowErrors.ThisParamBannedInConstructor);
        }
      }
    }

    pushClassPrivateMethod(
      classBody: N.ClassBody,
      method: N.ClassPrivateMethod,
      isGenerator: boolean,
      isAsync: boolean,
    ): void {
      if ((method: $FlowFixMe).variance) {
        this.unexpected((method: $FlowFixMe).variance.start);
      }
      delete (method: $FlowFixMe).variance;
      if (this.isRelational("<")) {
        method.typeParameters = this.flowParseTypeParameterDeclaration();
      }

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

    // parse a the super class type parameters and implements
    parseClassSuper(node: N.Class): void {
      super.parseClassSuper(node);
      if (node.superClass && this.isRelational("<")) {
        node.superTypeParameters = this.flowParseTypeParameterInstantiation();
      }
      if (this.isContextual("implements")) {
        this.next();
        const implemented: N.FlowClassImplements[] = (node.implements = []);
        do {
          const node = this.startNode();
          node.id = this.flowParseRestrictedIdentifier(/*liberal*/ true);
          if (this.isRelational("<")) {
            node.typeParameters = this.flowParseTypeParameterInstantiation();
          } else {
            node.typeParameters = null;
          }
          implemented.push(this.finishNode(node, "ClassImplements"));
        } while (this.eat(tt.comma));
      }
    }

    checkGetterSetterParams(method: 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(param.start, FlowErrors.GetterMayNotHaveThisParam);
        } else if (this.isThisParam(param)) {
          this.raise(param.start, FlowErrors.SetterMayNotHaveThisParam);
        }
      }
    }

    parsePropertyName(
      node: N.ObjectOrClassMember | N.ClassMember | N.TsNamedTypeElementBase,
      isPrivateNameAllowed: boolean,
    ): N.Identifier {
      const variance = this.flowParseVariance();
      const key = super.parsePropertyName(node, isPrivateNameAllowed);
      // $FlowIgnore ("variance" not defined on TsNamedTypeElementBase)
      node.variance = variance;
      return key;
    }

    // parse type parameters for object method shorthand
    parseObjPropValue(
      prop: N.ObjectMember,
      startPos: ?number,
      startLoc: ?Position,
      isGenerator: boolean,
      isAsync: boolean,
      isPattern: boolean,
      isAccessor: boolean,
      refExpressionErrors: ?ExpressionErrors,
    ): void {
      if ((prop: $FlowFixMe).variance) {
        this.unexpected((prop: $FlowFixMe).variance.start);
      }
      delete (prop: $FlowFixMe).variance;

      let typeParameters;

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

      super.parseObjPropValue(
        prop,
        startPos,
        startLoc,
        isGenerator,
        isAsync,
        isPattern,
        isAccessor,
        refExpressionErrors,
      );

      // add typeParameters if we found them
      if (typeParameters) {
        (prop.value || prop).typeParameters = typeParameters;
      }
    }

    parseAssignableListItemTypes(param: N.Pattern): N.Pattern {
      if (this.eat(tt.question)) {
        if (param.type !== "Identifier") {
          this.raise(param.start, FlowErrors.OptionalBindingPattern);
        }
        if (this.isThisParam(param)) {
          this.raise(param.start, FlowErrors.ThisParamMayNotBeOptional);
        }

        ((param: any): N.Identifier).optional = true;
      }
      if (this.match(tt.colon)) {
        param.typeAnnotation = this.flowParseTypeAnnotation();
      } else if (this.isThisParam(param)) {
        this.raise(param.start, FlowErrors.ThisParamAnnotationRequired);
      }

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

      this.resetEndLocation(param);
      return param;
    }

    parseMaybeDefault(
      startPos?: ?number,
      startLoc?: ?Position,
      left?: ?N.Pattern,
    ): N.Pattern {
      const node = super.parseMaybeDefault(startPos, startLoc, left);

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

      return node;
    }

    shouldParseDefaultImport(node: N.ImportDeclaration): boolean {
      if (!hasTypeImportKind(node)) {
        return super.shouldParseDefaultImport(node);
      }

      return isMaybeDefaultImport(this.state);
    }

    parseImportSpecifierLocal(
      node: N.ImportDeclaration,
      specifier: N.Node,
      type: string,
      contextDescription: string,
    ): void {
      specifier.local = hasTypeImportKind(node)
        ? this.flowParseRestrictedIdentifier(
            /* liberal */ true,
            /* declaration */ true,
          )
        : this.parseIdentifier();

      this.checkLVal(specifier.local, contextDescription, BIND_LEXICAL);
      node.specifiers.push(this.finishNode(specifier, type));
    }

    // parse typeof and type imports
    maybeParseDefaultImportSpecifier(node: N.ImportDeclaration): boolean {
      node.importKind = "value";

      let kind = null;
      if (this.match(tt._typeof)) {
        kind = "typeof";
      } else if (this.isContextual("type")) {
        kind = "type";
      }
      if (kind) {
        const lh = this.lookahead();

        // import type * is not allowed
        if (kind === "type" && lh.type === tt.star) {
          this.unexpected(lh.start);
        }

        if (
          isMaybeDefaultImport(lh) ||
          lh.type === tt.braceL ||
          lh.type === tt.star
        ) {
          this.next();
          node.importKind = kind;
        }
      }

      return super.maybeParseDefaultImportSpecifier(node);
    }

    // parse import-type/typeof shorthand
    parseImportSpecifier(node: N.ImportDeclaration): void {
      const specifier = this.startNode();
      const firstIdentLoc = this.state.start;
      const firstIdent = this.parseModuleExportName();

      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("as") && !this.isLookaheadContextual("as")) {
        const as_ident = this.parseIdentifier(true);
        if (
          specifierTypeKind !== null &&
          !this.match(tt.name) &&
          !this.state.type.keyword
        ) {
          // `import {type as ,` or `import {type as }`
          specifier.imported = as_ident;
          specifier.importKind = specifierTypeKind;
          specifier.local = as_ident.__clone();
        } else {
          // `import {type as foo`
          specifier.imported = firstIdent;
          specifier.importKind = null;
          specifier.local = this.parseIdentifier();
        }
      } else if (
        specifierTypeKind !== null &&
        (this.match(tt.name) || this.state.type.keyword)
      ) {
        // `import {type foo`
        specifier.imported = this.parseIdentifier(true);
        specifier.importKind = specifierTypeKind;
        if (this.eatContextual("as")) {
          specifier.local = this.parseIdentifier();
        } else {
          isBinding = true;
          specifier.local = specifier.imported.__clone();
        }
      } else {
        if (firstIdent.type === "StringLiteral") {
          throw this.raise(
            specifier.start,
            Errors.ImportBindingIsString,
            firstIdent.value,
          );
        }
        isBinding = true;
        specifier.imported = firstIdent;
        specifier.importKind = null;
        specifier.local = specifier.imported.__clone();
      }

      const nodeIsTypeImport = hasTypeImportKind(node);
      const specifierIsTypeImport = hasTypeImportKind(specifier);

      if (nodeIsTypeImport && specifierIsTypeImport) {
        this.raise(
          firstIdentLoc,
          FlowErrors.ImportTypeShorthandOnlyInPureImport,
        );
      }

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

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

      this.checkLVal(specifier.local, "import specifier", BIND_LEXICAL);
      node.specifiers.push(this.finishNode(specifier, "ImportSpecifier"));
    }

    parseBindingAtom(): N.Pattern {
      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: N.Function, allowModifiers?: boolean): void {
      // $FlowFixMe
      const kind = node.kind;
      if (kind !== "get" && kind !== "set" && this.isRelational("<")) {
        node.typeParameters = this.flowParseTypeParameterDeclaration();
      }
      super.parseFunctionParams(node, allowModifiers);
    }

    // parse flow type annotations on variable declarator heads - let foo: string = bar
    parseVarId(
      decl: N.VariableDeclarator,
      kind: "var" | "let" | "const",
    ): void {
      super.parseVarId(decl, kind);
      if (this.match(tt.colon)) {
        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,
      afterLeftParse?: Function,
      refNeedsArrowPos?: ?Pos,
    ): N.Expression {
      let state = null;

      let jsx;

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

        jsx = this.tryParse(
          () =>
            super.parseMaybeAssign(
              refExpressionErrors,
              afterLeftParse,
              refNeedsArrowPos,
            ),
          state,
        );
        /*:: invariant(!jsx.aborted) */

        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;
        if (context[context.length - 1] === tc.j_oTag) {
          context.length -= 2;
        } else if (context[context.length - 1] === tc.j_expr) {
          context.length -= 1;
        }
      }

      if (jsx?.error || this.isRelational("<")) {
        state = state || this.state.clone();

        let typeParameters;

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

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

              this.resetStartLocationFromNode(result, typeParameters);

              return result;
            },
          );

          // <T>(() => {}: any);
          if (
            arrowExpression.type !== "ArrowFunctionExpression" &&
            arrowExpression.extra?.parenthesized
          ) {
            abort();
          }

          // The above can return a TypeCastExpression when the arrow
          // expression is not wrapped in parens. See also `this.parseParenItem`.
          const expr = this.maybeUnwrapTypeCastExpression(arrowExpression);
          expr.typeParameters = typeParameters;
          this.resetStartLocationFromNode(expr, typeParameters);

          return arrowExpression;
        }, state);

        let arrowExpression: ?(
          | N.ArrowFunctionExpression
          | N.TypeCastExpression
        ) = null;

        if (
          arrow.node &&
          this.maybeUnwrapTypeCastExpression(arrow.node).type ===
            "ArrowFunctionExpression"
        ) {
          if (!arrow.error && !arrow.aborted) {
            // <T> async () => {}
            if (arrow.node.async) {
              /*:: invariant(typeParameters) */
              this.raise(
                typeParameters.start,
                FlowErrors.UnexpectedTypeParameterBeforeAsyncArrowFunction,
              );
            }

            return arrow.node;
          }

          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(
          typeParameters.start,
          FlowErrors.UnexpectedTokenAfterTypeParameter,
        );
      }

      return super.parseMaybeAssign(
        refExpressionErrors,
        afterLeftParse,
        refNeedsArrowPos,
      );
    }

    // handle return types for arrow functions
    parseArrow(node: N.ArrowFunctionExpression): ?N.ArrowFunctionExpression {
      if (this.match(tt.colon)) {
        const result = this.tryParse(() => {
          const oldNoAnonFunctionType = this.state.noAnonFunctionType;
          this.state.noAnonFunctionType = true;

          const typeNode = this.startNode();

          [
            // $FlowFixMe (destructuring not supported yet)
            typeNode.typeAnnotation,
            // $FlowFixMe (destructuring not supported yet)
            node.predicate,
          ] = this.flowParseTypeAndPredicateInitialiser();

          this.state.noAnonFunctionType = oldNoAnonFunctionType;

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

          return typeNode;
        });

        if (result.thrown) return null;
        /*:: invariant(result.node) */

        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(): boolean {
      return this.match(tt.colon) || super.shouldParseArrow();
    }

    setArrowFunctionParameters(
      node: N.ArrowFunctionExpression,
      params: N.Expression[],
    ): void {
      if (this.state.noArrowParamsConversionAt.indexOf(node.start) !== -1) {
        node.params = params;
      } else {
        super.setArrowFunctionParameters(node, params);
      }
    }

    checkParams(
      node: N.Function,
      allowDuplicates: boolean,
      isArrowFunction: ?boolean,
    ): void {
      if (
        isArrowFunction &&
        this.state.noArrowParamsConversionAt.indexOf(node.start) !== -1
      ) {
        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(node.params[i].start, FlowErrors.ThisParamMustBeFirst);
        }
      }

      return super.checkParams(...arguments);
    }

    parseParenAndDistinguishExpression(canBeArrow: boolean): N.Expression {
      return super.parseParenAndDistinguishExpression(
        canBeArrow && this.state.noArrowAt.indexOf(this.state.start) === -1,
      );
    }

    parseSubscripts(
      base: N.Expression,
      startPos: number,
      startLoc: Position,
      noCalls?: ?boolean,
    ): N.Expression {
      if (
        base.type === "Identifier" &&
        base.name === "async" &&
        this.state.noArrowAt.indexOf(startPos) !== -1
      ) {
        this.next();

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

        if (!arrow.error && !arrow.aborted) return arrow.node;

        const result = this.tryParse(
          () => super.parseSubscripts(base, startPos, 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, startPos, startLoc, noCalls);
    }

    parseSubscript(
      base: N.Expression,
      startPos: number,
      startLoc: Position,
      noCalls: ?boolean,
      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: N.OptionalCallExpression = this.startNodeAt(
          startPos,
          startLoc,
        );
        node.callee = base;
        node.typeArguments = this.flowParseTypeParameterInstantiation();
        this.expect(tt.parenL);
        // $FlowFixMe
        node.arguments = this.parseCallExpressionArguments(tt.parenR, false);
        node.optional = true;
        return this.finishCallExpression(node, /* optional */ true);
      } else if (
        !noCalls &&
        this.shouldParseTypes() &&
        this.isRelational("<")
      ) {
        const node = this.startNodeAt(startPos, startLoc);
        node.callee = base;

        const result = this.tryParse(() => {
          node.typeArguments = this.flowParseTypeParameterInstantiationCallOrNew();
          this.expect(tt.parenL);
          node.arguments = this.parseCallExpressionArguments(tt.parenR, false);
          if (subscriptState.optionalChainMember) node.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,
        startPos,
        startLoc,
        noCalls,
        subscriptState,
      );
    }

    parseNewArguments(node: N.NewExpression): void {
      let targs = null;
      if (this.shouldParseTypes() && this.isRelational("<")) {
        targs = this.tryParse(() =>
          this.flowParseTypeParameterInstantiationCallOrNew(),
        ).node;
      }
      node.typeArguments = targs;

      super.parseNewArguments(node);
    }

    parseAsyncArrowWithTypeParameters(
      startPos: number,
      startLoc: Position,
    ): ?N.ArrowFunctionExpression {
      const node = this.startNodeAt(startPos, startLoc);
      this.parseFunctionParams(node);
      if (!this.parseArrow(node)) return;
      return this.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(this.state.pos, FlowErrors.UnterminatedFlowComment);
      }
      return fileNode;
    }

    skipBlockComment(): void {
      if (this.hasPlugin("flowComments") && this.skipFlowComment()) {
        if (this.state.hasFlowComment) {
          this.unexpected(null, FlowErrors.NestedFlowComment);
        }
        this.hasFlowCommentCompletion();
        this.state.pos += this.skipFlowComment();
        this.state.hasFlowComment = true;
        return;
      }

      if (this.state.hasFlowComment) {
        const end = this.input.indexOf("*-/", (this.state.pos += 2));
        if (end === -1) {
          throw this.raise(this.state.pos - 2, Errors.UnterminatedComment);
        }
        this.state.pos = end + 3;
        return;
      }

      super.skipBlockComment();
    }

    skipFlowComment(): number | boolean {
      const { pos } = this.state;
      let shiftToFirstNonWhiteSpace = 2;
      while (
        [charCodes.space, charCodes.tab].includes(
          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(this.state.pos, Errors.UnterminatedComment);
      }
    }

    // Flow enum parsing

    flowEnumErrorBooleanMemberNotInitialized(
      pos: number,
      { enumName, memberName }: { enumName: string, memberName: string },
    ): void {
      this.raise(
        pos,
        FlowErrors.EnumBooleanMemberNotInitialized,
        memberName,
        enumName,
      );
    }

    flowEnumErrorInvalidMemberName(
      pos: number,
      { enumName, memberName }: { enumName: string, memberName: string },
    ): void {
      const suggestion = memberName[0].toUpperCase() + memberName.slice(1);
      this.raise(
        pos,
        FlowErrors.EnumInvalidMemberName,
        memberName,
        suggestion,
        enumName,
      );
    }

    flowEnumErrorDuplicateMemberName(
      pos: number,
      { enumName, memberName }: { enumName: string, memberName: string },
    ): void {
      this.raise(pos, FlowErrors.EnumDuplicateMemberName, memberName, enumName);
    }

    flowEnumErrorInconsistentMemberValues(
      pos: number,
      { enumName }: { enumName: string },
    ): void {
      this.raise(pos, FlowErrors.EnumInconsistentMemberValues, enumName);
    }

    flowEnumErrorInvalidExplicitType(
      pos: number,
      {
        enumName,
        suppliedType,
      }: { enumName: string, suppliedType: null | string },
    ) {
      return this.raise(
        pos,
        suppliedType === null
          ? FlowErrors.EnumInvalidExplicitTypeUnknownSupplied
          : FlowErrors.EnumInvalidExplicitType,
        enumName,
        suppliedType,
      );
    }

    flowEnumErrorInvalidMemberInitializer(
      pos: number,
      { enumName, explicitType, memberName }: EnumContext,
    ) {
      let message = null;
      switch (explicitType) {
        case "boolean":
        case "number":
        case "string":
          message = FlowErrors.EnumInvalidMemberInitializerPrimaryType;
          break;
        case "symbol":
          message = FlowErrors.EnumInvalidMemberInitializerSymbolType;
          break;
        default:
          // null
          message = FlowErrors.EnumInvalidMemberInitializerUnknownType;
      }
      return this.raise(pos, message, enumName, memberName, explicitType);
    }

    flowEnumErrorNumberMemberNotInitialized(
      pos: number,
      { enumName, memberName }: { enumName: string, memberName: string },
    ): void {
      this.raise(
        pos,
        FlowErrors.EnumNumberMemberNotInitialized,
        enumName,
        memberName,
      );
    }

    flowEnumErrorStringMemberInconsistentlyInitailized(
      pos: number,
      { enumName }: { enumName: string },
    ): void {
      this.raise(
        pos,
        FlowErrors.EnumStringMemberInconsistentlyInitailized,
        enumName,
      );
    }

    flowEnumMemberInit(): EnumMemberInit {
      const startPos = this.state.start;
      const endOfInit = () => this.match(tt.comma) || this.match(tt.braceR);
      switch (this.state.type) {
        case tt.num: {
          const literal = this.parseLiteral(this.state.value, "NumericLiteral");
          if (endOfInit()) {
            return { type: "number", pos: literal.start, value: literal };
          }
          return { type: "invalid", pos: startPos };
        }
        case tt.string: {
          const literal = this.parseLiteral(this.state.value, "StringLiteral");
          if (endOfInit()) {
            return { type: "string", pos: literal.start, value: literal };
          }
          return { type: "invalid", pos: startPos };
        }
        case tt._true:
        case tt._false: {
          const literal = this.parseBooleanLiteral();
          if (endOfInit()) {
            return {
              type: "boolean",
              pos: literal.start,
              value: literal,
            };
          }
          return { type: "invalid", pos: startPos };
        }
        default:
          return { type: "invalid", pos: startPos };
      }
    }

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

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

    flowEnumMembers({
      enumName,
      explicitType,
    }: {
      enumName: string,
      explicitType: EnumExplicitType,
    }): {|
      members: {|
        booleanMembers: Array<N.Node>,
        numberMembers: Array<N.Node>,
        stringMembers: Array<N.Node>,
        defaultedMembers: Array<N.Node>,
      |},
      hasUnknownMembers: boolean,
    |} {
      const seenNames = new Set();
      const members = {
        booleanMembers: [],
        numberMembers: [],
        stringMembers: [],
        defaultedMembers: [],
      };
      let hasUnknownMembers = false;
      while (!this.match(tt.braceR)) {
        if (this.eat(tt.ellipsis)) {
          hasUnknownMembers = true;
          break;
        }
        const memberNode = this.startNode();
        const { id, init } = this.flowEnumMemberRaw();
        const memberName = id.name;
        if (memberName === "") {
          continue;
        }
        if (/^[a-z]/.test(memberName)) {
          this.flowEnumErrorInvalidMemberName(id.start, {
            enumName,
            memberName,
          });
        }
        if (seenNames.has(memberName)) {
          this.flowEnumErrorDuplicateMemberName(id.start, {
            enumName,
            memberName,
          });
        }
        seenNames.add(memberName);
        const context = { enumName, explicitType, memberName };
        memberNode.id = id;
        switch (init.type) {
          case "boolean": {
            this.flowEnumCheckExplicitTypeMismatch(
              init.pos,
              context,
              "boolean",
            );
            memberNode.init = init.value;
            members.booleanMembers.push(
              this.finishNode(memberNode, "EnumBooleanMember"),
            );
            break;
          }
          case "number": {
            this.flowEnumCheckExplicitTypeMismatch(init.pos, context, "number");
            memberNode.init = init.value;
            members.numberMembers.push(
              this.finishNode(memberNode, "EnumNumberMember"),
            );
            break;
          }
          case "string": {
            this.flowEnumCheckExplicitTypeMismatch(init.pos, context, "string");
            memberNode.init = init.value;
            members.stringMembers.push(
              this.finishNode(memberNode, "EnumStringMember"),
            );
            break;
          }
          case "invalid": {
            throw this.flowEnumErrorInvalidMemberInitializer(init.pos, context);
          }
          case "none": {
            switch (explicitType) {
              case "boolean":
                this.flowEnumErrorBooleanMemberNotInitialized(
                  init.pos,
                  context,
                );
                break;
              case "number":
                this.flowEnumErrorNumberMemberNotInitialized(init.pos, context);
                break;
              default:
                members.defaultedMembers.push(
                  this.finishNode(memberNode, "EnumDefaultedMember"),
                );
            }
          }
        }

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

    flowEnumStringMembers(
      initializedMembers: Array<N.Node>,
      defaultedMembers: Array<N.Node>,
      { enumName }: { enumName: string },
    ): Array<N.Node> {
      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.flowEnumErrorStringMemberInconsistentlyInitailized(
            member.start,
            { enumName },
          );
        }
        return defaultedMembers;
      } else {
        for (const member of defaultedMembers) {
          this.flowEnumErrorStringMemberInconsistentlyInitailized(
            member.start,
            { enumName },
          );
        }
        return initializedMembers;
      }
    }

    flowEnumParseExplicitType({
      enumName,
    }: {
      enumName: string,
    }): EnumExplicitType {
      if (this.eatContextual("of")) {
        if (!this.match(tt.name)) {
          throw this.flowEnumErrorInvalidExplicitType(this.state.start, {
            enumName,
            suppliedType: null,
          });
        }

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

        if (
          value !== "boolean" &&
          value !== "number" &&
          value !== "string" &&
          value !== "symbol"
        ) {
          this.flowEnumErrorInvalidExplicitType(this.state.start, {
            enumName,
            suppliedType: value,
          });
        }

        return value;
      }
      return null;
    }

    flowEnumBody(node: N.Node, { enumName, nameLoc }): N.Node {
      const explicitType = this.flowEnumParseExplicitType({ enumName });
      this.expect(tt.braceL);
      const { members, hasUnknownMembers } = this.flowEnumMembers({
        enumName,
        explicitType,
      });
      node.hasUnknownMembers = hasUnknownMembers;

      switch (explicitType) {
        case "boolean":
          node.explicitType = true;
          node.members = members.booleanMembers;
          this.expect(tt.braceR);
          return this.finishNode(node, "EnumBooleanBody");
        case "number":
          node.explicitType = true;
          node.members = members.numberMembers;
          this.expect(tt.braceR);
          return this.finishNode(node, "EnumNumberBody");
        case "string":
          node.explicitType = true;
          node.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.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.flowEnumErrorInconsistentMemberValues(nameLoc, { enumName });
            return empty();
          }
        }
      }
    }

    flowParseEnumDeclaration(node: N.Node): N.Node {
      const id = this.parseIdentifier();
      node.id = id;
      node.body = this.flowEnumBody(this.startNode(), {
        enumName: id.name,
        nameLoc: id.start,
      });
      return this.finishNode(node, "EnumDeclaration");
    }

    updateContext(prevType: TokenType): void {
      if (
        this.match(tt.name) &&
        this.state.value === "of" &&
        prevType === tt.name &&
        this.input.slice(this.state.lastTokStart, this.state.lastTokEnd) ===
          "interface"
      ) {
        this.state.exprAllowed = false;
      } else {
        super.updateContext(prevType);
      }
    }

    // check if the next token is a tt.relation("<")
    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;
    }

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