{"record":{"id":"34f234df54064d00","repo":"facebook/flow","slug":"could-not-find-target-in-array-of-parent-type","errorCode":null,"errorMessage":"Could not find target in array of `${parent.type}.${key}`.","messagePattern":"Could not find target in array of `(.+?)\\.(.+?)`\\.","errorType":"exception","errorClass":"InvalidStatementError","httpStatus":null,"severity":"error","filePath":"packages/flow-transform/src/transform/mutations/utils/getStatementParent.js","lineNumber":60,"sourceCode":"    for (const key of invalidKeys) {\n      // $FlowExpectedError[prop-missing]\n      const value = parentWithType[key];\n\n      if (\n        // $FlowFixMe[invalid-compare]\n        value === target ||\n        (Array.isArray(value) && value.includes(target))\n      ) {\n        throw new InvalidStatementError(\n          `Attempted to insert a statement into \\`${parentWithType.type}.${key}\\`.`,\n        );\n      }\n    }\n  }\n  function getAssertedIndex(key: string, arr: ReadonlyArray<unknown>): number {\n    const idx = arr.indexOf(target);\n    if (idx === -1) {\n      throw new InvalidStatementError(\n        `Could not find target in array of \\`${parent.type}.${key}\\`.`,\n      );\n    }\n    return idx;\n  }\n\n  const parent = target.parent;\n  const result: StatementParent = (() => {\n    switch (parent.type) {\n      case 'IfStatement': {\n        assertValidStatementLocation(parent, 'test');\n        const key = parent.consequent === target ? 'consequent' : 'alternate';\n        return {type: 'single', parent, key};\n      }\n\n      case 'LabeledStatement': {\n        assertValidStatementLocation(parent, 'label');\n        return {type: 'single', parent, key: 'body'};","sourceCodeStart":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/facebook/flow/blob/d1341dac899a79c027762f6b423d896045287620/packages/flow-transform/src/transform/mutations/utils/getStatementParent.js#L42-L78","documentation":"Thrown by getAssertedIndex() inside getStatementParent() when the target's parent is a BlockStatement, Program, or SwitchCase but target is not present in the parent's body/consequent array (indexOf returns -1). This means the node is detached or stale: its .parent pointer references a container whose array no longer contains it, usually because the node was already removed or the array was mutated without keeping the parent link in sync. Statement mutations (InsertStatement, RemoveStatement, ReplaceStatementWithMany) require the target to be findable in its container to compute the insertion/removal index.","triggerScenarios":"Applying a second mutation whose target was already removed by an earlier mutation in the same batch; splicing parent.body manually (body.splice(i, 1)) without clearing node.parent and then calling a mutation on the removed node; holding node references from a traversal done before earlier mutations were applied.","commonSituations":"Batching multiple mutations computed from a single pre-mutation traversal, where two mutations touch overlapping nodes; mixing manual AST edits with the library's mutation API; reusing node references across transform passes.","solutions":["Re-traverse (or re-collect the node references) immediately before applying each mutation so targets are fresh.","When batching mutations, filter out any mutation whose target is no longer present in its parent array after earlier mutations run.","Never manually splice body/consequent arrays; express every change as a library mutation so parent links stay consistent.","Order mutations so that a remove/replace of a node never precedes another mutation that references the same node."],"exampleFix":"// before\nconst mutations = collectFromTraversal(ast); // stale references\napplyAll(mutations); // second mutation targets a node removed by the first\n\n// after\nfor (const mutation of collectFromTraversal(ast)) {\n  applyMutation(ast, mutation);          // one mutation per pass\n  ast = reparseOrRetraverse(ast);        // refresh references before the next\n}","handlingStrategy":"validation","validationCode":"function isStillInParentArray(node) {\n  const parent = node.parent;\n  if (parent == null) return false;\n  const arr = parent.type === 'SwitchCase' ? parent.consequent : parent.body;\n  return Array.isArray(arr) && arr.includes(node);\n}\n\nconst safeMutations = mutations.filter(m => isStillInParentArray(m.node ?? m.target));","typeGuard":"/** True when the node is still reachable from its declared array container. */\nfunction isAttachedArrayMember(node) {\n  const parent = node.parent;\n  if (parent == null) return false;\n  if (parent.type === 'SwitchCase') return parent.consequent.includes(node);\n  if (parent.type === 'BlockStatement' || parent.type === 'Program') {\n    return parent.body.includes(node);\n  }\n  return false;\n}","tryCatchPattern":"import {InvalidStatementError} from 'flow-transform/src/transform/Errors';\n\nfor (const mutation of mutations) {\n  try {\n    applyMutation(ast, mutation);\n  } catch (err) {\n    if (err instanceof InvalidStatementError && err.message.startsWith('Could not find target')) {\n      continue; // target already removed by an earlier mutation; skip\n    }\n    throw err;\n  }\n}","preventionTips":["Apply one mutation per traversal pass and re-collect node references between passes.","Never splice body/consequent arrays manually; use the mutation API so parent links stay consistent.","Before applying a queued mutation, re-check that the target is still present in its container."],"tags":["flow-transform","ast","codemod","stale-reference","mutation"],"backgroundTag":"detached-ast-node","analyzedSha":"d1341dac899a79c027762f6b423d896045287620","analyzedAt":"2026-08-17T00:07:02.212Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}