constructor({
    babelTypes,
    defaultFbtOptions,
    fileSource,
    nodeChecker,
    path,
    pluginOptions,
    validFbtExtraOptions,
  }: {
    babelTypes: BabelTypes,
    defaultFbtOptions: FbtCallSiteOptions,
    validFbtExtraOptions: $ReadOnly<FbtExtraOptionConfig>,
    fileSource: string,
    nodeChecker: FbtNodeChecker,
    path: NodePath,
    pluginOptions: PluginOptions,
  }): void {
    this.defaultFbtOptions = defaultFbtOptions;
    this.validFbtExtraOptions = validFbtExtraOptions;
    this.fileSource = fileSource;
    this.moduleName = nodeChecker.moduleName;
    this.node = path.node;
    this.nodeChecker = nodeChecker;
    this.path = path;
    this.pluginOptions = pluginOptions;
    this.t = babelTypes;
  }

  static create({
    babelTypes,
    defaultFbtOptions,
    fileSource,
    path,
    pluginOptions,
    validFbtExtraOptions,
  }: {
    babelTypes: BabelTypes,
    defaultFbtOptions: FbtCallSiteOptions,
    validFbtExtraOptions: $ReadOnly<FbtExtraOptionConfig>,
    fileSource: string,
    path: NodePath,
    pluginOptions: PluginOptions,
  }): ?FbtFunctionCallProcessor {
    const nodeChecker = FbtNodeChecker.forFbtFunctionCall(path.node);
    return nodeChecker != null
      ? new FbtFunctionCallProcessor({
          babelTypes,
          defaultFbtOptions,
          validFbtExtraOptions,
          fileSource,
          nodeChecker,
          path,
          pluginOptions,
        })
      : null;
  }

  _assertJSModuleWasAlreadyRequired(): this {
    const {moduleName, path} = this;
    if (!this.nodeChecker.isJSModuleBound<typeof path.node>(path)) {
      throw errorAt(
        path.node,
        `${moduleName} is not bound. Did you forget to require('${moduleName}')?`,
      );
    }
    return this;
  }

  _assertHasEnoughArguments(): this {
    const {moduleName, node} = this;
    if (node.arguments.length < 2) {
      throw errorAt(
        node,
        `Expected ${moduleName} calls to have at least two arguments. ` +
          `Only ${node.arguments.length} was given.`,
      );
    }
    return this;
  }

  _createFbtRuntimeCallForMetaPhrase(
    metaPhrases: $ReadOnlyArray<MetaPhrase>,
    metaPhraseIndex: number,
    stringVariationRuntimeArgs: StringVariationRuntimeArgumentBabelNodes,
  ): BabelNodeCallExpression {
    const {fbtNode, phrase} = metaPhrases[metaPhraseIndex];
    const {pluginOptions} = this;

    // 1st argument - Sentinel Payload
    const argsOutput = JSON.stringify(
      ({
        jsfbt: phrase.jsfbt,
        // $FlowFixMe[incompatible-cast]
        project: phrase.project,
      }: SentinelPayload),
    );
    const encodedOutput =
      pluginOptions.fbtBase64 === true
        ? Buffer.from(argsOutput).toString('base64')
        : argsOutput;
    const fbtSentinel = pluginOptions.fbtSentinel ?? SENTINEL;
    const args: Array<
      | BabelNodeExpression
      | BabelNodeSpreadElement
      | BabelNodeJSXNamespacedName
      | BabelNodeArgumentPlaceholder,
    > = [stringLiteral(fbtSentinel + encodedOutput + fbtSentinel)];

    // 2nd argument - `FbtTableArgs` in the fbt runtime calls
    const fbtRuntimeArgs = this._createFbtRuntimeArgumentsForMetaPhrase(
      metaPhrases,
      metaPhraseIndex,
      stringVariationRuntimeArgs,
    );
    if (fbtRuntimeArgs.length > 0) {
      args.push(arrayExpression(fbtRuntimeArgs));
    }

    // 3rd argument - Extra options which eventually become part of `FbtInputOpts`
    const extraOptionsNode = fbtNode.getExtraOptionsNode();
    if (extraOptionsNode != null) {
      if (args.length === 1) {
        args.push(nullLiteral());
      }
      args.push(
        objectExpression([
          objectProperty(identifier(EXTRA_OPTIONS_KEY), extraOptionsNode),
        ]),
      );
    }

    return callExpression(
      memberExpression(identifier(this.moduleName), identifier('_')),
      args,
    );
  }

  _createRootFbtRuntimeCall(
    metaPhrases: $ReadOnlyArray<MetaPhrase>,
  ): BabelNodeCallExpression | BabelNodeSequenceExpression {
    const stringVariationRuntimeArgs =
      this._createRuntimeArgsFromStringVariantNodes(metaPhrases[0]);
    if (!this._hasStringVariationAndContainsInnerString(metaPhrases)) {
      return this._createFbtRuntimeCallForMetaPhrase(
        metaPhrases,
        0,
        stringVariationRuntimeArgs,
      );
    }
    this._throwIfStringVariationArgsMayCauseSideEffects(metaPhrases);

    const stringVariationRuntimeArgIdentifiers =
      this._generateUniqueIdentifiersForRuntimeArgs(
        stringVariationRuntimeArgs.length,
      );
    const fbtRuntimeCall = this._createFbtRuntimeCallForMetaPhrase(
      metaPhrases,
      0,
      stringVariationRuntimeArgIdentifiers,
    );
    this._injectVariableDeclarationsForStringVariationArguments(
      stringVariationRuntimeArgIdentifiers,
    );
    return this._wrapFbtRuntimeCallInSequenceExpression(
      stringVariationRuntimeArgs,
      fbtRuntimeCall,
      stringVariationRuntimeArgIdentifiers,
    );
  }

  /**
   * String variation arguments are not allowed to contain anything that may
   * cause side-effects. Side-effects are mostly introduced by but not limited to
   * method calls and class instantiations. Please refer to the JSDoc of
   * FbtNode#throwIfAnyArgumentContainsFunctionCallOrClassInstantiation for
   * examples.
   */
  _throwIfStringVariationArgsMayCauseSideEffects(
    metaPhrases: $ReadOnlyArray<MetaPhrase>,
  ) {
    metaPhrases[0].compactStringVariations.array.map(svArg =>
      svArg.fbtNode.throwIfAnyArgumentContainsFunctionCallOrClassInstantiation(
        this.path.context.scope,
      ),
    );
  }

  _injectVariableDeclarationsForStringVariationArguments(
    identifiersForStringVariationRuntimeArgs: $ReadOnlyArray<BabelNodeIdentifier>,
  ): void {
    // Find the first ancestor block statement node or the program root node
    let curPath = this.path;
    while (!isBlockStatement(curPath.node) && !isProgram(curPath.node)) {
      // $FlowFixMe[incompatible-type-arg]
      curPath = nullthrows(
        curPath.parentPath,
        'curPath can not be null. Otherwise, it means we reached the root' +
          ' of Babel AST in the previous iteration and therefore we would have exited the loop.',
      );
    }
    const blockOrProgramPath = curPath;
    const blockOrProgramNode = blockOrProgramPath.node;
    invariant(
      isBlockStatement(blockOrProgramNode) || isProgram(blockOrProgramNode),
      "According to the above loop's condition, " +
        'blockOrProgramNode must be either a block statement or a program node ',
    );

    // Replace the blockStatement/program node with
    // a new blockStatement/program with injected declarations
    const declarations = variableDeclaration(
      'var',
      identifiersForStringVariationRuntimeArgs.map(identifier =>
        variableDeclarator(identifier),
      ),
    );
    const cloned = clone(blockOrProgramNode);
    cloned.body = [declarations, ...cloned.body];
    blockOrProgramPath.replaceWith(cloned);
  }

  /**
   * Pre-assign those arguments that create string variations to local variables,
   * and use references to these variables in fbt call. Note: Local variables
   * will be auto-declared in sequenceExpression.
   *
   * E.g.
   * Before:
   *   fbt._()
   *
   * After:
   *   (identifier_0 = runtimeArg1, identifier_1 = runtimeArg2, fbt._())
   */
  _wrapFbtRuntimeCallInSequenceExpression(
    runtimeArgs: $ReadOnlyArray<BabelNodeCallExpression>,
    fbtRuntimeCall: BabelNodeCallExpression,
    identifiersForStringVariationRuntimeArgs: $ReadOnlyArray<BabelNodeIdentifier>,
  ): BabelNodeSequenceExpression {
    invariant(
      runtimeArgs.length == identifiersForStringVariationRuntimeArgs.length,
      'Expect exactly one identifier for each string variation runtime argument. ' +
        'Instead we get %s identifiers and %s arguments.',
      identifiersForStringVariationRuntimeArgs.length,
      runtimeArgs.length,
    );
    const expressions = runtimeArgs
      .map((runtimeArg, i) =>
        assignmentExpression(
          '=',
          identifiersForStringVariationRuntimeArgs[i],
          runtimeArg,
        ),
      )
      .concat(fbtRuntimeCall);
    return sequenceExpression(expressions);
  }

  _hasStringVariationAndContainsInnerString(
    metaPhrases: $ReadOnlyArray<MetaPhrase>,
  ): boolean {
    const fbtElement = metaPhrases[0].fbtNode;
    invariant(
      fbtElement instanceof FbtElementNode,
      'Expected a FbtElementNode for top level string but received: %s',
      varDump(fbtElement),
    );
    const doesNotContainInnerString = fbtElement.children.every(child => {
      return !(child instanceof FbtImplicitParamNode);
    });
    if (doesNotContainInnerString) {
      return false;
    }

    return metaPhrases[0].compactStringVariations.array.length > 0;
  }

  _generateUniqueIdentifiersForRuntimeArgs(
    count: number,
  ): Array<BabelNodeIdentifier> {
    const identifiers = [];
    for (
      let identifierSuffix = 0, numIdentifierCreated = 0;
      numIdentifierCreated < count;
      identifierSuffix++
    ) {
      const name = `${STRING_VARIATION_RUNTIME_ARGUMENT_IDENTIFIER_PREFIX}_${identifierSuffix}`;
      if (this.path.context.scope.getBinding(name) == null) {
        identifiers.push(identifier(name));
        numIdentifierCreated++;
      }
    }
    return identifiers;
  }

  /**
   * Consolidate a list of string variation arguments under the following conditions:
   *
   * Enum variation arguments are consolidated to avoid creating duplicates of string variations
   * (from a candidate values POV)
   *
   * Other types of variation arguments are accepted as-is.
   */
  _compactStringVariationArgs(
    args: $ReadOnlyArray<AnyStringVariationArg>,
  ): CompactStringVariations {
    const indexMap = [];
    const array = args.filter((arg, i) => {
      if (arg.isCollapsible) {
        return false;
      }
      indexMap.push(i);
      return true;
    });

    return {
      array,
      indexMap,
    };
  }

  _getPhraseParentIndex(
    fbtNode: AnyFbtNode,
    list: $ReadOnlyArray<AnyFbtNode>,
  ): ?number {
    if (fbtNode.parent == null) {
      return null;
    }
    const parentIndex = list.indexOf(fbtNode.parent);
    invariant(
      parentIndex > -1,
      'Unable to find parent fbt node: node=%s',
      varDump(fbtNode),
    );
    return parentIndex;
  }

  /**
   * Generates a list of meta-phrases from a given FbtElement node
   */
  _metaPhrases(fbtElement: FbtElementNode): $ReadOnlyArray<MetaPhrase> {
    const stringVariationArgs = fbtElement.getArgsForStringVariationCalc();
    const jsfbtBuilder = new JSFbtBuilder(
      this.fileSource,
      stringVariationArgs,
      this.pluginOptions.reactNativeMode,
    );
    const argsCombinations = jsfbtBuilder.getStringVariationCombinations();
    const compactStringVariations = this._compactStringVariationArgs(
      argsCombinations[0] || [],
    );
    const jsfbtMetadata = jsfbtBuilder.buildMetadata(
      compactStringVariations.array,
    );
    const sharedPhraseOptions = this._getSharedPhraseOptions(fbtElement);
    return [fbtElement, ...fbtElement.getImplicitParamNodes()].map(
      (fbtNode, _index, list) => {
        try {
          const phrase = {
            ...sharedPhraseOptions,
            jsfbt: {
              // the order of JSFBT props matter for unit tests
              t: {},
              m: jsfbtMetadata,
            },
          };
          const svArgsMapList = [];

          (argsCombinations.length
            ? argsCombinations
            : emptyArgsCombinations
          ).forEach(argsCombination => {
            // collect text/description pairs
            const svArgsMap = new StringVariationArgsMap(argsCombination);
            const argValues = compactStringVariations.indexMap.map(
              originIndex => nullthrows(argsCombination[originIndex]?.value),
            );
            const leaf = ({
              desc: fbtNode.getDescription(svArgsMap),
              text: fbtNode.getText(svArgsMap),
            }: TableJSFBTTreeLeaf);

            const tokenAliases = fbtNode.getTokenAliases(svArgsMap);
            if (tokenAliases != null) {
              leaf.tokenAliases = tokenAliases;
            }

            if (fbtNode instanceof FbtElementNode) {
              // gather list of svArgsMap for all args combination for later sanity checks
              svArgsMapList.push(svArgsMap);
            } else if (this.pluginOptions.generateOuterTokenName === true) {
              leaf.outerTokenName = fbtNode.getTokenName(svArgsMap);
            }

            if (argValues.length) {
              addLeafToTree<TableJSFBTTreeLeaf, TableJSFBTTree>(
                phrase.jsfbt.t,
                argValues,
                leaf,
              );
            } else {
              // jsfbt only contains one leaf
              phrase.jsfbt.t = leaf;
            }
          });

          if (fbtNode instanceof FbtElementNode) {
            fbtNode.assertNoOverallTokenNameCollision(svArgsMapList);
          }

          return {
            compactStringVariations,
            fbtNode,
            parentIndex: this._getPhraseParentIndex(fbtNode, list),
            phrase,
          };
        } catch (error) {
          throw errorAt(fbtNode.node, error);
        }
      },
    );
  }

  /**
   * Process current `fbt()` callsite (BabelNode) to generate:
   * - an `fbt._()` callsite or a sequencExpression that eventually returns an `fbt._()` callsite
   * - a list of meta-phrases describing the collected text strings from this fbt() callsite
   */
  convertToFbtRuntimeCall(): {
    // Client-side fbt._() call(or the sequencExpression that contains it)
    // usable in a web browser generated from the given fbt() callsite
    callNode: BabelNodeCallExpression | BabelNodeSequenceExpression,
    // List of phrases collected from the fbt() callsite
    metaPhrases: $ReadOnlyArray<MetaPhrase>,
  } {
    const fbtElement = this._convertToFbtNode();
    const metaPhrases = this._metaPhrases(fbtElement);
    const callNode = this._createRootFbtRuntimeCall(metaPhrases);
    return {
      callNode,
      metaPhrases,
    };
  }

  /**
   * fbt constructs are not allowed to be direct children of fbt constructs.
   * For example it is not okay to have
   *    <fbt desc='desc'>
   *      <fbt:param name="outer">
   *        <fbt:param name="inner">
   *          variable
   *        </fbt:param>
   *      </fbt:param>
   *    </fbt>
   * However, the next example is okay because the inner `fbt:param` sits inside
   * an inner fbt.
   *    <fbt desc='outer string'>
   *      <fbt:param name="outer">
   *        <fbt desc='inner string'>
   *          <fbt:param name="inner">
   *            variable
   *          </fbt:param>
   *        </fbt>
   *      </fbt:param>
   *    </fbt>
   */
  throwIfExistsNestedFbtConstruct(): void {
    this.path.traverse(
      {
        CallExpression(path: NodePathOf<BabelNodeCallExpression>) {
          // eslint-disable-next-line max-len
          // $FlowFixMe[object-this-reference] Babel transforms run with the plugin context by default
          const nodeChecker = (this.nodeChecker: FbtNodeChecker);
          const constructs = [
            FbtNodeType.Enum,
            FbtNodeType.Name,
            FbtNodeType.Param,
            FbtNodeType.Plural,
            FbtNodeType.Pronoun,
            FbtNodeType.SameParam,
          ];

          const childFbtConstructName =
            nodeChecker.getFbtConstructNameFromFunctionCall(path.node);
          if (!constructs.includes(childFbtConstructName)) {
            return;
          }

          let parentPath = path.parentPath;
          while (parentPath != null) {
            const parentNode = parentPath.node;
            if (
              FbtNodeChecker.forFbtFunctionCall(parentNode) != null ||
              // children JSX fbt aren't yet converted to function call by JSXFbtProcessor
              FbtNodeChecker.forJSXFbt(parentNode) != null
            ) {
              return;
            }

            const parentFbtConstructName =
              nodeChecker.getFbtConstructNameFromFunctionCall(parentNode);
            if (constructs.includes(parentFbtConstructName)) {
              throw errorAt(
                parentNode,
                `Expected fbt constructs to not nest inside fbt constructs, ` +
                  `but found ` +
                  `${nodeChecker.moduleName}.${(nullthrows(
                    childFbtConstructName,
                  ): string)} ` +
                  `nest inside ` +
                  `${nodeChecker.moduleName}.${(nullthrows(
                    parentFbtConstructName,
                  ): string)}`,
              );
            }
            parentPath = parentPath.parentPath;
          }
        },
      },
      {
        nodeChecker: this.nodeChecker,
      },
    );
  }

  /**
   * Converts current fbt() BabelNode to an FbtNode equivalent
   */
  _convertToFbtNode(): FbtElementNode {
    this._assertJSModuleWasAlreadyRequired();
    this._assertHasEnoughArguments();

    const {moduleName, node} = this;
    const {arguments: fbtCallArgs} = node;
    const fbtContentsNode = convertToStringArrayNodeIfNeeded(
      moduleName,
      fbtCallArgs[0],
    );
    // $FlowFixMe[cannot-write]
    fbtCallArgs[0] = fbtContentsNode;

    const elementNode = FbtElementNode.fromBabelNode({
      moduleName,
      node,
      validExtraOptions: this.validFbtExtraOptions,
    });
    if (elementNode == null) {
      throw errorAt(
        node,
        `${moduleName}: unable to create FbtElementNode from given Babel node`,
      );
    }
    return elementNode;
  }
This class provides utility methods to process the babel node of the standard fbt function call
(i.e. `fbt(...)`)