{"record":{"id":"428bf552b2d50b94","repo":"swc-project/swc","slug":"failed-to-restore-source-context-because-the-sourc","errorCode":null,"errorMessage":"failed to restore source context because the source length changed while passing AST between JavaScript and native code","messagePattern":"failed to restore source context because the source length changed while passing AST between JavaScript and native code","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"bindings/binding_core_node/src/ast_context.rs","lineNumber":88,"sourceCode":"}\n\npub fn prepare_program_with_context(\n    c: &Compiler,\n    mut program: Program,\n    source_context: ProgramSourceContext,\n) -> Result<(swc_core::common::sync::Lrc<SourceFile>, Program), Error> {\n    let fm = c.cm.new_source_file(\n        source_context.file_name().into(),\n        source_context.source.clone(),\n    );\n\n    let expected_len = source_context\n        .end_pos\n        .checked_sub(source_context.start_pos)\n        .context(\"invalid source context byte range\")?;\n\n    if fm.byte_length() != expected_len {\n        bail!(\n            \"failed to restore source context because the source length changed while passing AST \\\n             between JavaScript and native code\"\n        );\n    }\n\n    rebase_program_spans(&mut program, &source_context, &fm)\n        .context(\"failed to rebase AST spans to restored source context\")?;\n\n    Ok((fm, program))\n}\n\nfn rebase_program_spans(\n    program: &mut Program,\n    source_context: &ProgramSourceContext,\n    fm: &SourceFile,\n) -> Result<(), Error> {\n    let old_start = source_context.start_pos;\n    let old_end = source_context.end_pos;","sourceCodeStart":70,"sourceCodeEnd":106,"githubUrl":"https://github.com/swc-project/swc/blob/5176682b65416c6b5de6b47379ae1588ea3ecb3f/bindings/binding_core_node/src/ast_context.rs#L70-L106","documentation":"The Node binding (@swc/core native layer) can accept an already-parsed program instead of raw source: JS sends a ProgramEnvelope { program, sourceContext } and bindings/binding_core_node rebuilds a SourceFile from sourceContext.source, then verifies the fresh file's byte length equals the recorded range (endPos - startPos) before rebasing every AST span into the new source map (prepare_program_with_context). A mismatch triggers this bail! — SWC spans are raw byte offsets into the global source map, so a length drift would silently corrupt all spans and sourcemaps, and the check refuses to proceed.","triggerScenarios":"Calling a @swc/core Node API that receives a parsed program envelope where the source string differs in byte length from the one the AST's spans were produced against — the code was edited, re-encoded (CRLF->LF, BOM added/removed), or replaced with another file's text between the parse and the call.","commonSituations":"Mutating the code string after parse() and reusing the old AST; normalizing line endings or BOM between parse and print; mixing spans from the original source with a minified/preprocessed string; version skew between the JS layer and the native binding altering envelope semantics.","solutions":["Pass the byte-exact same source string that produced the AST's spans (freeze it in a const immediately after parsing).","Re-parse the modified source instead of reusing the stale AST — fresh spans always match the new text.","Normalize line endings/BOM once, before parsing, and feed the identical normalized string to every subsequent call.","Align @swc/core JS and native package versions so envelope handling matches."],"exampleFix":"// before: source mutated between parse and native call\nconst ast = await parse(src);\nsrc = src.replaceAll(\"\\r\\n\", \"\\n\"); // length changed -> span corruption\nawait print(ast, { sourceContext });\n\n// after: normalize first, then parse, then reuse both unchanged\nsrc = src.replaceAll(\"\\r\\n\", \"\\n\");\nconst ast = await parse(src);\nawait print(ast, { sourceContext });","handlingStrategy":"try-catch","validationCode":"import { Buffer } from \"node:buffer\";\n// run before any @swc/core call that receives a parsed program envelope\nexport function sourceContextIsConsistent({ source, startPos, endPos }) {\n  const start = Number(startPos ?? 0);\n  const end = Number(endPos ?? 0);\n  if (!Number.isInteger(start) || !Number.isInteger(end) || end < start) return false;\n  return Buffer.byteLength(source, \"utf8\") === end - start;\n}\nif (!sourceContextIsConsistent(envelope.sourceContext)) {\n  throw new Error(\"source drifted from AST spans — re-parse before calling native\");\n}","typeGuard":"/**\n * Narrows a ProgramSourceContext to one whose byte length matches its span range,\n * i.e. safe to pass to the native restore path.\n */\nfunction isRestorableSourceContext(c) {\n  return Buffer.byteLength(c.source, \"utf8\") === c.endPos - c.startPos;\n}","tryCatchPattern":"try {\n  result = await callNativeWithProgram(program, sourceContext);\n} catch (e) {\n  if (/failed to restore source context/.test(String(e?.message ?? e))) {\n    const reparsed = await parse(sourceContext.source); // regenerate spans from current text\n    result = await callNativeWithProgram(reparsed.program, sourceContext);\n  } else {\n    throw e;\n  }\n}","preventionTips":["Freeze the source string immediately after parse (const) and never normalize it afterward","Normalize line endings and BOM once, before parsing, then reuse the identical string everywhere","Never reuse an AST after editing the source — re-parse instead","Keep @swc/core JS and native layer versions in lockstep"],"tags":["node-bindings","spans","source-map","ast-roundtrip","napi"],"backgroundTag":"source-length-mismatch","analyzedSha":"5176682b65416c6b5de6b47379ae1588ea3ecb3f","analyzedAt":"2026-08-17T16:16:52.067Z","contentChangedAt":"2026-08-17T16:16:52.067Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}