{"record":{"id":"3a6070ab0706bfab","repo":"facebook/flow","slug":"message","errorCode":null,"errorMessage":"${message}","messagePattern":"\\$\\{message\\}","errorType":"exception","errorClass":"NodeIsDeletedError","httpStatus":null,"severity":"error","filePath":"packages/flow-transform/src/transform/MutationContext.js","lineNumber":65,"sourceCode":"      `Attempted to mutate a \\`${node.type}.${key}\\` when it has already been mutated.`,\n    );\n\n    const map = Array.isArray(\n      // $FlowExpectedError[prop-missing]\n      node[key],\n    )\n      ? this._mutatedArrays\n      : this._mutatedKeys;\n\n    map.set(node, map.get(node)?.add(key) ?? new Set([key]));\n  }\n\n  /**\n   * Throws if the node has been deleted\n   */\n  assertNotDeleted(node: ESNode, message: string): void {\n    if (this._deletedNodes.has(node)) {\n      throw new NodeIsDeletedError(message);\n    }\n  }\n\n  /**\n   * Throws if the key of the node has been mutated\n   */\n  assertNotMutated(node: ESNode, key: string, message: string): void {\n    if (this._mutatedKeys.get(node)?.has(key) === true) {\n      throw new NodeIsMutatedError(message);\n    }\n  }\n\n  appendCommentToSource(comment: Comment, placement: CommentPlacement): void {\n    this.code = appendCommentToSource(this.code, comment, placement);\n  }\n}\n","sourceCodeStart":47,"sourceCodeEnd":82,"githubUrl":"https://github.com/facebook/flow/blob/f88ac94bcf6992f5d5a158854d94613ebb92c6e6/packages/flow-transform/src/transform/MutationContext.js#L47-L82","documentation":"NodeIsDeletedError is flow-transform's fail-fast guard against mutating an AST node that a previous mutation in the same transform run already deleted or replaced. `MutationContext.markDeletion` puts nodes in a `_deletedNodes` set; `assertNotDeleted` (called from `markMutation` and from insert/replace operations) throws with the caller-supplied message, typically \"Attempted to mutate a `<type>.<key>` on a deleted node.\" or \"Attempted to insert before/after a deleted node\".","triggerScenarios":"Within one transform() run: `mutate.removeNode(stmt)` followed by another mutation that touches the same node or its subtree - e.g. insertStatementBefore on the removed statement, replaceNode on one of its descendants, or two visitors (enter and `:exit`) both returning mutations for the same node.","commonSituations":"Writing codemods with flow-transform/flow-upgrade: collecting nodes during traversal and mutating them all afterwards (some get removed then mutated again); a replace followed by an insert relative to the original node; visitors matching overlapping selectors that both try to delete/modify the same construct.","solutions":["Issue at most one mutation per node per transform run; delete last after reads.","After a replace, use the replacement node for any follow-up mutations, never the original.","If you need remove-then-insert semantics, use a single `replaceStatementWithMany` with the full new statement list instead.","Do not return mutations from both an enter and an `:exit` handler for the same selector."],"exampleFix":"// before - two mutations touch the same node\nif (path.node.type === 'ExpressionStatement') {\n  mutate.removeNode(path.node);\n  mutate.insertStatementBefore(path.node, buildLog()); // throws NodeIsDeletedError\n}\n\n// after - single replace mutation\nmutate.replaceStatementWithMany(path.node, [buildLog()]);","handlingStrategy":"validation","validationCode":"// track nodes you have already retired in this run\nconst retired = new Set();\n\nfunction safeRemove(mutate, node) {\n  if (retired.has(node)) return; // already deleted/replaced\n  retired.add(node);\n  mutate.removeNode(node);\n}\n\nfunction safeInsertBefore(mutate, anchor, stmt) {\n  if (retired.has(anchor)) return; // inserting around a deleted node throws\n  mutate.insertStatementBefore(anchor, stmt);\n}","typeGuard":null,"tryCatchPattern":"try {\n  transform(code, visitors);\n} catch (e) {\n  if (e.name === 'NodeIsDeletedError') {\n    // a later mutation touched a subtree an earlier mutation removed:\n    // find the two visitors fighting over the same node and reorder/dedupe\n    throw new Error(`codemod conflict: ${e.message}`, {cause: e});\n  }\n  throw e;\n}","preventionTips":["One mutation per node per transform run; track retired nodes in a Set.","Never return mutations from both enter and :exit handlers of the same selector.","After a replace, mutate the replacement node, not the original.","Prefer a single replaceStatementWithMany over remove-then-insert sequences."],"tags":["flow","flow-transform","codemod","ast-mutation","mutation-tracking"],"backgroundTag":"invalid-ast-mutation","analyzedSha":"f88ac94bcf6992f5d5a158854d94613ebb92c6e6","analyzedAt":"2026-08-20T10:41:37.992Z","schemaVersion":2},"datasetVersion":"2026-08-23T06:17:17.905Z"}