facebook/flow · error · InvalidStatementError

Attempted to insert a statement into `${parentWithType.type}

Error message

Attempted to insert a statement into `${parentWithType.type}.${key}`.

What it means

Thrown by getStatementParent() (used by the InsertStatement, RemoveStatement, and ReplaceStatementWithMany mutations) when the target node lives in a non-statement slot of its parent, such as the test of an IfStatement/WhileStatement, the init/test/update of a ForStatement, the label of a LabeledStatement, the object of a WithStatement, the left/right of a ForIn/ForOf, or the test of a SwitchCase. assertValidStatementLocation() checks each of these invalid keys and rejects the mutation because statements cannot be inserted relative to a node that is itself an expression or identifier position. The error names the exact offending slot (e.g. `IfStatement.test`) so you can see where the node actually sits.

Source

Thrown at packages/flow-transform/src/transform/mutations/utils/getStatementParent.js:51

    },
>;

export function getStatementParent(
  target: ModuleDeclaration | Statement,
): StatementParent {
  function assertValidStatementLocation<
    T extends Readonly<interface {type: string}>,
  >(parentWithType: T, ...invalidKeys: ReadonlyArray<keyof T>): void {
    for (const key of invalidKeys) {
      // $FlowExpectedError[prop-missing]
      const value = parentWithType[key];

      if (
        // $FlowFixMe[invalid-compare]
        value === target ||
        (Array.isArray(value) && value.includes(target))
      ) {
        throw new InvalidStatementError(
          `Attempted to insert a statement into \`${parentWithType.type}.${key}\`.`,
        );
      }
    }
  }
  function getAssertedIndex(key: string, arr: ReadonlyArray<unknown>): number {
    const idx = arr.indexOf(target);
    if (idx === -1) {
      throw new InvalidStatementError(
        `Could not find target in array of \`${parent.type}.${key}\`.`,
      );
    }
    return idx;
  }

  const parent = target.parent;
  const result: StatementParent = (() => {
    switch (parent.type) {

View on GitHub (pinned to d1341dac89)

Solutions

  1. Pass a node that actually occupies a statement position: a member of Program.body / BlockStatement.body, an IfStatement consequent/alternate, a loop body, or a SwitchCase.consequent entry.
  2. Before building the mutation, check which property of target.parent holds the target; if it is test/init/update/label/object/left/right, target the enclosing statement instead.
  3. If you intended to operate on the expression itself, use an expression-level transform (replace the parent statement with a new one containing the desired expression) rather than a statement insertion/removal mutation.
  4. In a codemod pipeline, type-narrow traversed nodes to Statement/ModuleDeclaration before constructing mutations.

Example fix

// before
const mutation = {
  kind: 'remove_statement',
  node: ifStatement.test, // BinaryExpression in a non-statement slot
};

// after
const mutation = {
  kind: 'remove_statement',
  node: ifStatement, // remove the whole statement, or target a body/consequent member
};
Defensive patterns

Strategy: type-guard

Validate before calling

import {FlowVisitorKeys} from 'flow-ast';

const STATEMENT_CONTAINER_SLOTS = {
  IfStatement: ['consequent', 'alternate'],
  LabeledStatement: ['body'],
  WithStatement: ['body'],
  DoWhileStatement: ['body'],
  WhileStatement: ['body'],
  ForStatement: ['body'],
  ForInStatement: ['body'],
  ForOfStatement: ['body'],
  SwitchCase: ['consequent'],
  BlockStatement: ['body'],
  Program: ['body'],
};

function isStatementPosition(node) {
  const parent = node.parent;
  if (parent == null) return false;
  const slots = STATEMENT_CONTAINER_SLOTS[parent.type];
  if (slots == null) return false;
  return slots.some(key => {
    const v = parent[key];
    return v === node || (Array.isArray(v) && v.includes(node));
  });
}

Type guard

/** True when `node` sits in a statement slot of its parent and statement mutations are safe. */
function isStatementPosition(node) {
  const parent = node.parent;
  if (parent == null) return false;
  switch (parent.type) {
    case 'IfStatement':
      return parent.consequent === node || parent.alternate === node;
    case 'LabeledStatement':
    case 'WithStatement':
    case 'DoWhileStatement':
    case 'WhileStatement':
    case 'ForStatement':
    case 'ForInStatement':
    case 'ForOfStatement':
      return parent.body === node;
    case 'SwitchCase':
      return parent.consequent.includes(node);
    case 'BlockStatement':
    case 'Program':
      return parent.body.includes(node);
    default:
      return false;
  }
}

Try / catch

import {InvalidStatementError} from 'flow-transform/src/transform/Errors';

try {
  applyMutation(ast, mutation);
} catch (err) {
  if (err instanceof InvalidStatementError && /Attempted to insert/.test(err.message)) {
    // target is in a non-statement slot; retarget or skip
    console.warn('skipping non-statement target:', err.message);
  } else {
    throw err;
  }
}

Prevention

When it happens

Trigger: Creating a mutation whose target is an expression node, e.g. RemoveStatement({node: ifStatement.test}), InsertStatement({target: forStatement.init, ...}), or ReplaceStatementWithMany({target: labeledStatement.label, ...}). Any of these hits assertValidStatementLocation because the target is stored in the parent's test/init/update/label/object/left/right property rather than a statement container.

Common situations: Codemods that traverse to a node via a visitor (e.g. matching BinaryExpression or Identifier) and then feed that node into a statement mutation API; selecting the if-test or loop-condition because it matched first; porting Babel/jscodeshift code where replaceWith() worked on arbitrary nodes and assuming the statement APIs behave the same.

Related errors


AI-assisted analysis of facebook/flow@d1341dac89 (2026-08-17). Data as JSON: /api/errors/7b0b048b4f23119c. Report an issue: GitHub.