Skip to content

test: fix toMatchInlineSnapshot through a same-file tail-call helper - #34978

Closed
robobun wants to merge 7 commits into
mainfrom
farm/8ec28035/fix-inline-snapshot-tail-call
Closed

test: fix toMatchInlineSnapshot through a same-file tail-call helper#34978
robobun wants to merge 7 commits into
mainfrom
farm/8ec28035/fix-inline-snapshot-tail-call

Conversation

@robobun

@robobun robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Fixes #2763.

Problem

function snap(v) { return expect(v).toMatchInlineSnapshot(); }
test("inline snapshot via helper", () => { snap({ helper: true }); });
error: Failed to update inline snapshot: Could not find 'toMatchInlineSnapshot' here
    at helper.test.ts:2:44

The error is reported at the test body's line (where snap(...) is called), not at the matcher call site inside the helper, and the snapshot can never be written. Jest 30 writes the snapshot at the helper's line.

Cause

return expect(v).toMatchInlineSnapshot() is a tail call. JSC's proper tail calls eliminate the helper's stack frame, so when Bun__CallFrame__getCallerSrcLoc walks the stack from inside the native matcher it sees the helper's caller (the test body) as the first JS frame. V8 doesn't implement PTC, so Jest never hits this.

The same helper without return (so the matcher is not in tail position) already works:

function snap(v) { expect(v).toMatchInlineSnapshot(); }   // writes correctly

Fix

Capture the expect(...) call site on the Expect object at construction time, while the helper's frame is still on the stack (the subsequent .toMatchInlineSnapshot() is the tail call; expect() itself is not). The capture uses a new Bun__CallFrame__getCallerSrcLocUnmapped that skips the sourcemap remap so the per-expect() cost is a one-frame StackVisitor step and a string ref bump; the remap is deferred to the inline-snapshot path via Bun__remapSrcLoc.

At writeback time, if the matcher-time location doesn't land on the matcher name, parse the expression at the expect-time location and read the matcher's EDot.name_loc off the AST. Parsing (rather than byte-scanning) keeps a .toMatchInlineSnapshot( substring inside a string, template, or comment argument from being matched. The resolved (line, col) then flows through the unchanged writeback loop.

The direct-call path is unchanged: the matcher-time location is exact there, so the fallback is never consulted and the existing byte-safe writeback (decoy strings, CRLF/BOM, astral, two-on-one-line) is preserved.

Verification

New cases in test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts cover:

  • same-file helper in tail position (toMatchInlineSnapshot and toThrowErrorMatchingInlineSnapshot)
  • nested tail-call helpers (return inner(v))
  • mixed with direct calls in the same test (sort order)
  • two calls to the same helper with different values (same-line conflict error)
  • decoy .toMatchInlineSnapshot( inside a string and comment argument

All six fail on the released binary and pass with the fix. Full snapshot.test.ts (68 tests) and expect.test.js (415 tests) pass.

The unrelated error snapshots assertion in the same file is made colour-agnostic so the file passes without FORCE_COLOR=1 (CI sets it; local bun bd test does not).


[review] gate passed · iteration 2 · 5 files touched

fails on main (without fix)
ASAN without fix: 6 failed, 1 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts
bun test v1.4.0 (79c3bc73c)

test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts:
(pass) most types [63.00ms]
(pass) should work with expect.anything() [0.73ms]
(pass) snapshots > dollars [1315.68ms]
(pass) snapshots > backslash [1321.40ms]
(pass) snapshots > dollars curly [1300.85ms]
(pass) snapshots > dollars curly 2 [1309.13ms]
(pass) snapshots > stuff [1338.66ms]
(pass) snapshots > stuff 2 [1310.35ms]
(pass) snapshots > regexp 1 [1317.82ms]
(pass) snapshots > regexp 2 [1313.45ms]
(pass) snapshots > string [1312.68ms]
(pass) snapshots > string with newline [1300.95ms]
(pass) snapshots > null byte [1311.42ms]
(pass) snapshots > null byte 2 [1287.41ms]
(pass) snapshots > backticks [1300.50ms]
(pass) snapshots > unicode surrogate halves [1295.55ms]
(pass) snapshots > property matchers [1312.73ms]
(pass) snapshots > jest newline oddity [2178.04ms]
(pass) snapshots > don't grow file on error [466.36ms]
(pass) snapshots > replaces file that fails to parse when update flag i
... (truncated)

release without fix: 1 skipped
bun test v1.4.0-canary.1 (0dfefe1d3)

test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts:
(pass) most types [1.06ms]
(pass) should work with expect.anything()
(pass) snapshots > dollars [36.57ms]
(pass) snapshots > backslash [35.51ms]
(pass) snapshots > dollars curly [40.17ms]
(pass) snapshots > dollars curly 2 [41.09ms]
(pass) snapshots > stuff [44.72ms]
(pass) snapshots > stuff 2 [51.08ms]
(pass) snapshots > regexp 1 [37.74ms]
(pass) snapshots > regexp 2 [45.47ms]
(pass) snapshots > string [48.67ms]
(pass) snapshots > string with newline [51.40ms]
(pass) snapshots > null byte [45.62ms]
(pass) snapshots > null byte 2 [43.25ms]
(pass) snapshots > backticks [51.93ms]
(pass) snapshots > unicode surrogate halves [43.84ms]
(pass) snapshots > property matchers [54.62ms]
(pass) snapshots > jest newline oddity [68.34ms]
(pass) snapshots > don't grow file on error [14.50ms]
(pass) snapshots > replaces file that fails to parse when update flag is used [26.17ms]
(pass) snapshots > grow file for new snapshot [140.30ms]
(pass) snapshots > backtick in test name [23.06ms]
(pass) snapshots > dollars curly in test name [24.00ms]
(pass) snapshots > #15283 [22.81ms]
(pass) sn
... (truncated)
passes on PR (with fix)
ASAN with fix: 1 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts
bun test v1.4.0 (79c3bc73c)

test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts:
(pass) most types [63.59ms]
(pass) should work with expect.anything() [0.77ms]
(pass) snapshots > dollars [1311.19ms]
(pass) snapshots > backslash [1291.61ms]
(pass) snapshots > dollars curly [1290.98ms]
(pass) snapshots > dollars curly 2 [1299.24ms]
(pass) snapshots > stuff [1316.06ms]
(pass) snapshots > stuff 2 [1293.87ms]
(pass) snapshots > regexp 1 [1291.68ms]
(pass) snapshots > regexp 2 [1299.34ms]
(pass) snapshots > string [1295.20ms]
(pass) snapshots > string with newline [1300.04ms]
(pass) snapshots > null byte [1305.86ms]
(pass) snapshots > null byte 2 [1309.16ms]
(pass) snapshots > backticks [1282.16ms]
(pass) snapshots > unicode surrogate halves [1296.17ms]
(pass) snapshots > property matchers [1305.35ms]
(pass) snapshots > jest newline oddity [2189.29ms]
(pass) snapshots > don't grow file on error [472.30ms]
(pass) snapshots > replaces file that fails to parse when update flag i
... (truncated)

release with fix: 1 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 643ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/136] cc obj/packages/bun-usockets/src/bsd.c.o
[2/136] cc obj/packages/bun-usockets/src/crypto/openssl.c.o
[3/136] cc obj/packages/bun-usockets/src/eventing/epoll_kqueue.c.o
[4/136] cc obj/packages/bun-usockets/src/eventing/libuv.c.o
[5/136] cc obj/packages/bun-usockets/src/quic.c.o
[6/136] cc obj/packages/bun-usockets/src/loop.c.o
[7/136] cc obj/packages/bun-usockets/src/udp.c.o
[8/136] cc obj/packages/bun-usockets/src/socket.c.o
[9/136] cc obj/src/jsc/bindings/uv-posix-polyfills.c.o
[10/136] cc obj/packages/bun-usockets/src/context.c.o
[11/136] cc obj/src/jsc/bindings/node/http/llhttp/http.c.o
[12/136] cc obj/packages/bun-usockets/src/fault_inject.c.o
[13/136] cc obj/src/jsc/bindings/uv-posix-stubs.c.o
[14/136] cc obj/src/jsc/bindings/node/http/llhttp/api.c.o
[15/136] cc obj/vendor/picohttpparser/picohttpparser.c.o
[16/136] cc obj/src/jsc/bindings/node/http/llhttp/llhttp.c.o
[17/136] gen cpp.rs (cppbind)
[18/136] gen generated_host_exports.rs
generated_host_exports.rs: 91 exports (host=3, lazy=10, gene
... (truncated)
diff hotspot
src/jsc/CallFrame.rs                               |  38 +++++
 src/jsc/bindings/bindings.cpp                      |  37 ++++-
 src/runtime/test_runner/expect.rs                  |  38 +++++
 src/runtime/test_runner/snapshot.rs                | 185 +++++++++++++++++++--
 .../test/snapshot-tests/snapshots/snapshot.test.ts | 105 +++++++++++-
 5 files changed, 386 insertions(+), 17 deletions(-)

gate history · 5 passed · 1 rejected · iteration 2

evidence per changed file
file                                                      reads  edits  tests
src/jsc/CallFrame.rs                                          3      4      0
src/jsc/bindings/bindings.cpp                                 2      4      0
src/runtime/test_runner/expect.rs                             6     10      0
src/runtime/test_runner/snapshot.rs                          10     13      0
…t/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts      5      4      0

JSC's proper tail calls eliminate the helper's frame when the matcher is
called as `return expect(v).toMatchInlineSnapshot()`, so the matcher's
caller-frame source location points at the helper's caller (the test
body) instead of the matcher call site, and writeback fails with
"Could not find 'toMatchInlineSnapshot' here".

Capture the `expect(...)` call site on the Expect object (that frame is
still on the stack when expect() runs), and at writeback time, when the
matcher-time location doesn't land on the matcher name, scan forward
from the expect-time location for `.<matcher>(` and use that instead.

The direct-call path is unchanged: the matcher-time location is exact
there, so the fallback is never consulted and the existing decoy-string
handling is preserved.
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Inline snapshot handling now captures expect(...) source locations, resolves matcher positions using fallback coordinates with UTF-16 columns, and adds tests for tail-call helpers, conflicting snapshots, and ANSI-dependent errors.

Inline snapshot location handling

Layer / File(s) Summary
Capture and propagate expect locations
src/jsc/CallFrame.rs, src/jsc/bindings/bindings.cpp, src/runtime/test_runner/expect.rs, src/runtime/test_runner/snapshot.rs
Caller locations can be captured without remapping, remapped separately, stored by Expect, and forwarded as same-file fallback coordinates.
Resolve matcher locations
src/runtime/test_runner/snapshot.rs
Inline snapshot writing validates matcher positions, recovers them from fallback coordinates, converts offsets to UTF-16 locations, and sorts resolved entries.
Validate tail-call snapshots
test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts
Tests cover helper-based inline snapshots, nested calls, decoy text, conflicting updates, and ANSI-sensitive error output.

Possibly related PRs

  • oven-sh/bun#32631: Updates runtime test-result handling using execution stack-entry context, which is related to resolving test runtime call locations.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR also changes an unrelated ANSI-color snapshot assertion, which is outside the tail-call/unicode fix. Move the color-agnostic snapshot update to a separate PR or justify it as required for #2763.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The code addresses the linked #2763 unicode-related location issue with UTF-16-aware fallback resolution.
Title check ✅ Passed The title clearly summarizes the main change: fixing inline snapshot updates for same-file tail-call helpers.
Description check ✅ Passed The description covers the problem, cause, fix, and verification, matching the template content even though headings differ.

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 3:14 PM PT - Jul 21st, 2026

@robobun, your commit 79c3bc73cf766db3a55ce152e55e7e6ea4ce7342 passed in Build #77112! 🎉


🧪   To try this PR locally:

bunx bun-pr 34978

That installs a local version of the PR into your bun-34978 executable, so you can run:

bun-34978 --bun

robobun added 2 commits July 21, 2026 17:59
The last assertion snapshots a matcher error message that includes ANSI
colour codes only when colours are enabled. CI sets FORCE_COLOR=1 so it
passes there, but a plain 'bun bd test' run without FORCE_COLOR sees the
plain-text message and fails the snapshot. Gate the snapshot form on
Bun.enableANSIColors and fall back to a substring match otherwise.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/runtime/test_runner/snapshot.rs`:
- Around line 98-173: Add focused unit tests for find_matcher_call and
byte_offset_to_line_col. Verify matcher detection with whitespace around the
property dot and opening parenthesis, and verify line/column calculations for
CRLF sequences and non-BMP characters counted as UTF-16 surrogate pairs. Keep
the tests local to these helpers and cover both successful matching and expected
offsets.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 333bf800-1965-479e-96c2-24b3213886dd

📥 Commits

Reviewing files that changed from the base of the PR and between 98fb0ac and f3721b4.

📒 Files selected for processing (3)
  • src/runtime/test_runner/expect.rs
  • src/runtime/test_runner/snapshot.rs
  • test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts

Comment thread src/runtime/test_runner/snapshot.rs Outdated
Comment thread src/runtime/test_runner/expect.rs Outdated
Comment thread src/runtime/test_runner/snapshot.rs Outdated
robobun and others added 2 commits July 21, 2026 19:08
…k scan

expect() hot path: capture the caller frame with a new
Bun__CallFrame__getCallerSrcLocUnmapped that skips the sourcemap remap
(two mutex acquisitions + hashmap lookup + VLQ search) and defer the
remap to inline_snapshot() via Bun__remapSrcLoc. The per-expect() cost
is now a single-frame StackVisitor step + computeLineAndColumn + one
WTFString ref.

Fallback scan: parse the expression at the expect() location and read
the matcher's EDot.name_loc off the AST instead of byte-scanning, so a
'.toMatchInlineSnapshot(' substring inside a string/template/comment
argument cannot be matched. Covered by a new decoy test case.
Comment thread test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/runtime/test_runner/snapshot.rs (2)

475-602: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Fallback resolution logic is correct and handles arbitrary helper-nesting depth.

Parsing always starts at the real expect(...) token (captured before the tail call), so nested/chained helpers resolve identically to a single-level helper. The dot.name == ils.kind + byte-prefix re-validation give reasonable defense against a malformed/garbage parse. This matches the PR's stated coverage (tail-call helpers, nested helpers, decoy text) and is validated by the extensive e2e tests added in snapshot.test.ts.

Nit: the comments at Lines 478-483 (6 lines) and Lines 513-519 (7 lines) exceed the 3-line guideline. Deferring to the consolidated comment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/test_runner/snapshot.rs` around lines 475 - 602, The fallback
resolution implementation is correct; only reduce the oversized comments in the
fallback-resolution loop. Consolidate the introductory explanation and the
parsing explanation into concise comments of no more than three lines each,
preserving the key rationale and behavior without changing code.

Source: Coding guidelines


1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Several newly-added comments exceed the repo's 3-line comment limit.

All six sites share the same root cause: this PR's explanatory comments (ownership contracts / design rationale for the tail-call fallback) are well-justified content-wise but run longer than the project's "Keep code comments to three lines or fewer" rule.

  • src/runtime/test_runner/snapshot.rs#L475-483: trim the "2a. resolve fallback locations…" block to ≤3 lines (currently 9 total, 6 newly added).
  • src/jsc/bindings/bindings.cpp#L6150-6153: trim the Bun__remapSrcLoc ownership comment from 4 to ≤3 lines.
  • src/jsc/CallFrame.rs#L184-187: trim the get_caller_src_loc_unmapped doc comment from 4 to ≤3 lines.
  • src/runtime/test_runner/expect.rs#L735-741: trim the "Capture the expect(...) call site…" comment (now 6 lines total) back to ≤3 lines.
  • src/runtime/test_runner/expect.rs#L1171-1177: trim the "Fallback location…" comment (now 6 lines total) back to ≤3 lines.
  • src/runtime/test_runner/snapshot.rs#L513-519: trim the "Parse expect(<args>)…" comment from 7 to ≤3 lines.

As per coding guidelines, "Keep code comments to three lines or fewer; put longer explanations in documentation only when explicitly requested."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/test_runner/snapshot.rs` at line 1, Reduce the six newly added
explanatory comments to no more than three lines each while preserving their
essential ownership, fallback, and call-site rationale. Update the comments at
the identified sites near `Bun__remapSrcLoc`, `get_caller_src_loc_unmapped`, the
snapshot fallback and argument parsing logic, and both `expect` fallback
locations; make no code changes.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/jsc/bindings/bindings.cpp`:
- Around line 6137-6170: Shorten the explanatory comment above
Bun__CallFrame__getCallerSrcLocUnmapped to three lines or fewer while preserving
that it skips sourcemap remapping for the expect() hot path and that remapping
is deferred to inline-snapshot writeback.

In `@src/jsc/CallFrame.rs`:
- Around line 184-188: Shorten the documentation comment for
get_caller_src_loc_unmapped to three lines or fewer while preserving the
essential description that it skips sourcemap remapping and is intended for hot
paths. Remove secondary implementation details or defer them to the consolidated
documentation.

In `@src/runtime/test_runner/expect.rs`:
- Around line 1171-1177: Shorten the comment immediately above the fallback_line
and fallback_col calculation to three lines or fewer, preserving only the
essential explanation of the fallback location and remap ownership behavior.
Remove redundant detail while leaving the implementation unchanged.
- Around line 735-741: Shorten the comment immediately above
get_caller_src_loc_unmapped in the expect call path to no more than three lines,
retaining only the essential explanation that the caller frame must be captured
before tail-position matchers run and that remapping is deferred for
inline_snapshot.

---

Outside diff comments:
In `@src/runtime/test_runner/snapshot.rs`:
- Around line 475-602: The fallback resolution implementation is correct; only
reduce the oversized comments in the fallback-resolution loop. Consolidate the
introductory explanation and the parsing explanation into concise comments of no
more than three lines each, preserving the key rationale and behavior without
changing code.
- Line 1: Reduce the six newly added explanatory comments to no more than three
lines each while preserving their essential ownership, fallback, and call-site
rationale. Update the comments at the identified sites near `Bun__remapSrcLoc`,
`get_caller_src_loc_unmapped`, the snapshot fallback and argument parsing logic,
and both `expect` fallback locations; make no code changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f6cf3f15-1fad-440c-9cde-8de1c10c3b9c

📥 Commits

Reviewing files that changed from the base of the PR and between f3721b4 and 0dfefe1.

📒 Files selected for processing (5)
  • src/jsc/CallFrame.rs
  • src/jsc/bindings/bindings.cpp
  • src/runtime/test_runner/expect.rs
  • src/runtime/test_runner/snapshot.rs
  • test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts

Comment thread src/jsc/bindings/bindings.cpp
Comment thread src/jsc/CallFrame.rs Outdated
Comment thread src/runtime/test_runner/expect.rs Outdated
Comment thread src/runtime/test_runner/expect.rs Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All three earlier findings (hot-path remap cost, byte-scan decoy, no-colour precision loss) are addressed and I found nothing new. Deferring because this still adds an unconditional StackVisitor::visit + computeLineAndColumn + toStringRef to every expect() call — a residual hot-path tradeoff a maintainer should sign off on — and the parser-driven fallback in snapshot.rs carries enough unsafe/MaybeUninit/FFI-ownership surface to warrant a human look.

What was reviewed:

  • Bun__remapSrcLoc in/out +1 ownership vs remap_stack_frame_positions — early-return leaves the caller's ref intact; dupe_ref + OwnedString guard in inline_snapshot releases whichever ref comes back.
  • Fallback ordering: (line, col) is rewritten before the sort, so mixed helper/direct calls stay monotone through the writeback loop.
  • byte_offset_to_line_col round-trips with the existing line_col_to_byte_offset (CRLF, LS/PS, astral → 2 UTF-16 units).
  • expect_src_file.deref() is wired into finalize(); new struct fields don't leak.
Extended reasoning...

Overview

Fixes #2763: toMatchInlineSnapshot() called through a same-file helper in tail position couldn't locate its own call site because JSC's proper tail calls eliminate the helper's frame. The fix captures the expect(...) call site (un-remapped, via a new Bun__CallFrame__getCallerSrcLocUnmapped) at Expect construction time, stores it on the struct, and at writeback time — if the matcher-time location doesn't land on the matcher name — parses the expression at the expect-time location and reads the matcher's EDot.name_loc off the AST. Touches src/jsc/CallFrame.rs (new FFI wrappers), src/jsc/bindings/bindings.cpp (split getCallerSrcLoc into remapping/non-remapping variants + standalone Bun__remapSrcLoc), src/runtime/test_runner/expect.rs (three new Expect fields, capture in call(), remap+compare in inline_snapshot()), src/runtime/test_runner/snapshot.rs (~130-line pre-sort fallback resolution loop + byte_offset_to_line_col), and six new test cases in snapshot.test.ts.

Security risks

None identified. Input is the test file's own source text (already trusted — it was just executed) and a JSC-provided stack location. The fallback parses via the real JS parser rather than byte-scanning (my earlier decoy concern was addressed in 7ce651d), so a .toMatchInlineSnapshot( substring inside a string/comment argument can't misdirect the writeback.

Level of scrutiny

High. Two independent reasons:

  1. Residual hot-path cost. My earlier 🔴 finding was addressed by dropping the sourcemap remap (mutexes + VLQ search) from the per-expect() path, but the unmapped capture still runs StackVisitor::visit, computeLineAndColumn, and one Bun::toStringRef on every expect() — the hottest native call in bun test. Whether that residual cost is acceptable, or whether the capture should be gated (e.g. only when the current file has inline snapshots), is a design call for a maintainer, not a bot.
  2. Non-trivial memory/FFI surface. New in/out +1 BunString ownership across Bun__remapSrcLoc; three new owned fields on Expect with a .deref() in finalize(); a MaybeUninit<TSXParser> + scopeguard + raw-pointer Log dance duplicated from the existing writeback loop. I traced each of these and they look correct, but the density of unsafe in a file-rewriting path is exactly where a second pair of eyes pays off.

Other factors

All prior review threads (mine and CodeRabbit's) are resolved. Test coverage is good — six targeted cases including the decoy-in-argument case, nested tail calls, mixed with direct calls, and the same-line conflict error — and the PR evidence shows the full 68-test file passing on both debug+ASAN and release. The unrelated error snapshots colour-branch now keeps full-precision inline snapshots on both arms (0dfefe1). The direct-call path is unchanged (fallback only engages when the primary location misses), so regression risk to existing behaviour is low.

@robobun

robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

CI on 79c3bc7 (build #77112): 193 jobs passed. The one red lane is darwin-14-aarch64-test-bun which expired without ever being picked up by an agent (started_at: null), and the six [flaky]-tagged tests (fs-promises-file-handle-readFile, gc-http-client-timeout, spawn, dev-server-ssr-100, websocket-server, multi-run) are unrelated to this diff and all passed on retry. snapshot.test.ts is green on every lane that ran. Ready for review.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

#39111 fixes the expression-bodied callback shape of this problem (test("x", () => expect(v).toMatchInlineSnapshot()), where there is no JS frame left at all) by recording the location when the matcher property is read instead of capturing on every expect() call, and its tests include the same-file helper case from this PR. If that approach is preferred, this PR can be closed in favour of it.

robobun added a commit that referenced this pull request Aug 16, 2026
Nested tail-calling helpers, a helper mixed with direct matcher calls,
the matcher name appearing inside the expect() argument, and one helper
called with two different values (same-line conflict error). All four
fail on the released binary with "Could not find 'toMatchInlineSnapshot'
here" at the helper's call site.

Also make the "error snapshots" assertion hold with colours off, so the
file passes under a plain `bun bd test` as well as under CI's
FORCE_COLOR=1.
@robobun

robobun commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favour of #39111, which fixes the same tail-call problem for this shape (same-file helper) as well as for the expression-bodied callback shape (test("x", () => expect(v).toMatchInlineSnapshot())), where no JS frame is left at all and the capture-at-expect() approach here still fails. The test cases from this PR (helper in tail position with both matchers, nested helpers, helper mixed with direct calls, the matcher name inside the argument, and the same-line conflict error) now live in #39111's snapshot.test.ts and pass there; the colour-independent error snapshots assertion is carried over too. This branch has also drifted into conflict with main.

@robobun robobun closed this Aug 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants