facebook/flow · error · Error

Expected to find the ${target.type} as a direct child of the

Error message

Expected to find the ${target.type} as a direct child of the ${parent.type}.

What it means

After confirming target.parent exists, the helper scans every visitor key of the parent looking for the target either as a direct property or inside an array property; if the target is not found anywhere, this 'should never happen' invariant error is thrown. In practice it means the parent pointer is stale: the parent used to hold the node but no longer does, typically because another transform already replaced or removed it.

Source

Thrown at packages/flow-parser/oxidized-src/transform/astNodeMutationHelpers.js:73

    } else if (
      Array.isArray(
        // $FlowExpectedError[prop-missing]
        parent[key],
      )
    ) {
      for (let i = 0; i < parent[key].length; i += 1) {
        // $FlowExpectedError[invalid-tuple-index]
        const current = parent[key][i];
        // $FlowFixMe[invalid-compare]
        if (current === target) {
          return {type: 'array', node: parent, key, targetIndex: i};
        }
      }
    }
  }

  // this shouldn't happen ever
  throw new Error(
    `Expected to find the ${target.type} as a direct child of the ${parent.type}.`,
  );
}

/**
 * Replace a node with a new node within an AST (via the parent pointer).
 */
export function replaceNodeOnParent(
  originalNode: ESNode,
  originalNodeParent: ESNode,
  nodeToReplaceWith: ESNode | ReadonlyArray<ESNode>,
  visitorKeys?: ?VisitorKeysType,
): void {
  const replacementParent = getParentKey(
    originalNode,
    originalNodeParent,
    visitorKeys,
  );

View on GitHub (pinned to d1341dac89)

Solutions

  1. Re-parse the source (or re-traverse a pristine copy of the AST) for each independent transform pass
  2. Never cache node references across transforms; re-find nodes by position or type in each pass
  3. Do all changes for one node in a single visitor callback instead of multiple passes

Example fix

// before
const targets = findNodes(ast, 'Foo');
ast = SimpleTransform.transform(ast, visitorA);
mutateAll(targets); // stale references into transformed tree

// after
ast = SimpleTransform.transform(ast, visitorA);
const targets = findNodes(ast, 'Foo'); // re-find on the new tree
mutateAll(targets);
Defensive patterns

Strategy: try-catch

Type guard

const isDirectChild = (parent, target, visitorKeys) =>
  (visitorKeys[parent.type] || []).some(k =>
    parent[k] === target || (Array.isArray(parent[k]) && parent[k].includes(target))
  );

Try / catch

try {
  SimpleTransform.transform(ast, options);
} catch (e) {
  if (e.message.includes('as a direct child')) {
    // invariant corruption: start over from source instead of patching
    ast = parse(originalSource, {});
    SimpleTransform.transform(ast, safeOptions);
  } else throw e;
}

Prevention

When it happens

Trigger: Two transforms (or two steps of one codemod) sharing node references, where the first replaced the node on its parent and the second then tries to mutate the same node; an AST mutated directly by hand so that parent arrays no longer contain the nodes they point to.

Common situations: Running multiple SimpleTransform passes while caching node references between them; codemod frameworks that queue up node-level operations and flush them after the tree has already changed.

Related errors


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