node:vm: keep a vm SyntaxError's parse location out of the source map of the file its filename names - #38362
node:vm: keep a vm SyntaxError's parse location out of the source map of the file its filename names#38362robobun wants to merge 1 commit into
Conversation
…ugh the file its filename names A parser SyntaxError only keeps the URL of the source it came from, and JSC's addErrorInfo() formats the stack before handing the error back, so the formatter used to remap the <parse> frame and err.line of every SyntaxError by URL. For node:vm code that URL is whatever filename the caller passed; when it names a file Bun transpiled, the vm code's own line came back remapped through that file's source map. node:vm now builds its parse errors through NodeVM::createParseError(), which tags the instance with a private vmParseError property before addErrorInfo() runs, and the formatter leaves tagged errors at the parser's position. createModuleRecord() also clears an exception a user Error.prepareStackTrace may have left behind before throwing, as the Script and compileFunction paths already did.
|
Warning Review limit reached
Next review available in: 19 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (7)
Comment |
|
Status
|
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Because it touches the shared stack-trace formatter (FormatStackTraceForJS.cpp) and adds new JSC exception-scope handling across three node:vm compile paths, a human look would still be worthwhile — particularly given the several open PRs the description names as editing the same <parse> branch and toErrorObject() sites.
What was reviewed:
createParseError()replicatesParserError::toErrorObject()'s SyntaxError path (createSyntaxError+addErrorInfo(line())+setParseError) — the dropped-1overrideLineNumber arg is equivalent to passingparseError.line().- The formatter's new
getDirect(vmParseErrorPrivateName)check is only reached whenerrorInstanceis non-null, which excludes the GC-finalizer path (computeErrorInfoToStringpasses nullptr). - The
sb.append(remappedFrame.source_url...)→sb.append(sourceURLForFrame)change: for the tagged branch this correctly useserr->sourceURL(); for the untouched remap branchsourceURLForFramewas already reassigned from the remapped URL, so no behavior change there. createModuleRecord()'s newtryClearException()matches the existing pattern at the other two call sites.
Extended reasoning...
Overview
This PR fixes incorrect line numbers on SyntaxErrors thrown when compiling node:vm code under a filename that Bun has already loaded (and thus holds a source map for). It adds a private vmParseError builtin symbol, a NodeVM::createParseError() helper that tags the error before addErrorInfo() materializes the stack, wires the three vm compile paths (new Script, compileFunction, SourceTextModule) through it, and teaches formatStackTrace()'s <parse> branch to skip source-map remapping when the tag is present. It also removes a redundant nested isEmpty() check and switches the <parse> frame's URL append to use sourceURLForFrame (which is now correct for both the tagged and remapped branches). createModuleRecord() gains the same tryClearException() handling for a throwing user Error.prepareStackTrace that the other two sites already have. A comprehensive subprocess test covers all six vm entry points under both a loaded and an unloaded filename.
Security risks
None. The private symbol is not JS-observable; the change only affects which line number is reported in a compile-time SyntaxError's stack string. No user input reaches new parsing/allocation paths.
Level of scrutiny
Moderate-to-high. FormatStackTraceForJS.cpp is invoked for every error's .stack materialization across the runtime, so a mistake there would have broad reach. The new C++ also involves JSC exception-scope discipline (tryClearException / RETURN_IF_EXCEPTION ordering) at three sites. While the change is focused and the mechanism (private-symbol tag, mirroring the existing vmErrorDecorated pattern) is clean, this is not a mechanical fix.
Other factors
- The
uncheckedDowncast<ErrorInstance>(createSyntaxError(...))is safe: JSC'screateSyntaxErroruses the internal error structure, unaffected by user overrides ofglobalThis.SyntaxError. - Setting
hasSet = truefor the tagged branch leaves thelineout-param at the valueaddErrorInfo()seeded it with (parseError.line()), which is what becomeserr.line— matching the test's assertion of the physical line 5. - The PR description names six other open PRs touching the same
<parse>branch ortoErrorObject()call sites; a maintainer should confirm merge order / rebase expectations. - The test is well-constructed (20-line comment padding guarantees any remapped line would exceed 20, so a false pass is not possible) and covers the full API matrix plus
Bun.inspect.
Problem
new vm.Script(src, { filename: __filename })withsrcfailing on its line 10 getserr.line === 5andat <parse> (/tmp/remap_syntax_short.js:5)inerr.stack(5 being the line of the script that line 10 of its transpiled output maps back to; it varies with the host file), while the same source compiled under a name Bun never loaded reports 10.err.originalLineis set to 10. The arrow header node:vm prepends (/tmp/remap_syntax_short.js:10plus the source line) is right, so the error contradicts itself. Same forvm.runInThisContext/runInContext/runInNewContext(all built onScript) andvm.compileFunction. Repro in the details block.index.js.formatStackTrace()(src/jsc/bindings/FormatStackTraceForJS.cpp, the<parse>branch) builds a frame out oferr->sourceURL()/err->line()and passes it toBun__remapStackFramePositions, which is keyed by URL, and then overwritesline(which becomeserr.line) with the result.ParserError::toErrorObject()calls JSC'saddErrorInfo(), which copies the provider's URL and the line onto theErrorInstanceand materializes.stack(running Bun's formatter) before returning the error to node:vm, so theNodeVMScriptFetcherthat marks the provider as vm code (the signal node:vm: don't remap vm code through the source map of the file its filename names #38344 uses for runtime frames) is not reachable from the formatter, and node:vm gets the error back only after the string has been built.vm.SourceTextModulecompiles with no URL today, so its parse errors took the other path through the same branch: nothing remapped,hasSetstayed false, and the frame loop then published the first caller frame as the error's location (err.line === 225,err.sourceURL === "node:vm"for a parse error on line 5 of the module source).Fix
NodeVM::createParseError()(src/jsc/bindings/NodeVM.cpp) replaces the threetoErrorObject()calls (new Script,compileFunction,SourceTextModule). For a SyntaxError it does whattoErrorObject()does (createSyntaxError+addErrorInfo+setParseError), but puts a privatevmParseErrorproperty on the instance beforeaddErrorInfo()runs; every other ParserError type is still delegated totoErrorObject().<parse>branch skips the remap for a tagged error and setshasSet, so the frame showserr->sourceURL()and the parser's line,err.linekeeps the parser's line, and nooriginalLineis written (originalLinemeans a map was applied; node:vm: don't remap vm code through the source map of the file its filename names #38344 stops writing it for runtime frames in vm code for the same reason). Today an unloaded name gets an identity remap andoriginalLine === line; that goes away too.err.linenor a<parse>frame), which is now also what Bun's header,<parse>frame anderr.lineagree on.Error.captureStackTrace, or the error printer once it can mark string-parsed frames as vm frames) can still tell.vmParseErroris a private symbol like node:vm's existingvmErrorDecorated, so it is not observable from JS.createModuleRecord()now clears an exception left by a throwing userError.prepareStackTracebefore throwing the SyntaxError, asconstructScript()/constructAnonymousFunction()already do; without it the new test's SourceTextModule case aborts underBUN_JSC_validateExceptionChecks=1(which CI sets for this file on the ASAN lane). node:vm: apply SourceTextModule lineOffset/columnOffset like Node and name frames after the identifier #38235 makes the same change at this site.test/js/node/vm/vm.test.ts, "SyntaxError from compiling vm code under the filename of a file Bun transpiled". A fixture starting with a 20-line comment (so every line its source map can produce is > 20) compiles a source failing on line 5 under its own path and under an unloaded name through all six APIs and checks the header,<parse>frame,err.line,err.originalLine,err.sourceURLand the<parse>frameBun.inspectprints. Fails on the released build with line 23 for the own-name cases; passes with this change, also underBUN_JSC_validateExceptionChecks=1.vm.test.ts,vm-sourceUrl.test.ts,test/js/bun/test/stack.test.ts,test/js/node/v8/capture-stack-trace.test.js, thetest-vm-syntax-error-*,test-vm-module-errors,test-vm-source-map-urlandtest-repl-{recoverable,syntax-error-*,unexpected-token-recoverable}node tests.inspect-error.test.jshas two minified-file snapshot failures that are the known debug-buildrequireframe, unrelated.ZigStackFrame; with it, the printer can flag the<parse>frame of a tagged error when it parses the stack string. Open PRs error: don't add a synthetic <parse> frame to SyntaxErrors that aren't parser errors #37386 / error: gate the <parse> stack frame on the parser having recorded a location, not on sourceURL #37432 / error printer: show file/line for JSC parser SyntaxErrors with no stack #36634 / Release the stack frame strings leaked by the C++ source map remap callers #37459 edit the same<parse>branch for other reasons and compose with this; node:vm: report compileFunction body errors as SyntaxErrors and reject bodies that close the function #38239 / node:vm: throw runInContext/runInNewContext compile errors from the context's realm #38317, which touch thetoErrorObject()call sites, would switch tocreateParseError()on rebase.Background
Bun__remapStackFramePositionstakes (URL, line, column) and, if Bun holds a source map for that URL, rewrites the position to the original file;err.originalLineis where the formatter stores the pre-remap line when it does this.<parse>frame: for a SyntaxError raised by JSC's parser there is no stack frame inside the rejected source, so Bun'sformatStackTrace()synthesizes anat <parse> (url:line)line from the locationaddErrorInfo()recorded on the instance, and treats that location as the error'sline/sourceURL.addErrorInfo(vm, error, line, SourceCode)(JSCruntime/Error.cpp): stores the source's URL and the line on theErrorInstanceand immediately materializes.stack, which invokes Bun'scomputeErrorInfohook with only the frames, the line/column, the URL and the instance.decorateParseErrorStack()/handleException()(NodeVM.cpp) prepend Node'surl:line+ source line + caret to.stackafter the error exists, computed from the ParserError directly, which is why it was already right.src/js/builtins/BunBuiltinNames.h): per-VM private symbols C++ can use as property keys that JS code cannot name or enumerate.Repro output on the released build
vm.compileFunctionon the released build, same source on line 5 of the body:line: 2, originalLine: 5, at <parse> (/tmp/remap_module.js:2).new vm.SourceTextModuleon the released build, parse error on line 5:line: 225, originalLine: 225, sourceURL: "node:vm"; this branch:line: 5, nooriginalLine/sourceURL.