Skip to content

strings: include unterminated final line in error code-frame preview - #36683

Open
robobun wants to merge 3 commits into
mainfrom
farm/dc26a4ab/fix-codeframe-no-trailing-newline
Open

robobun wants to merge 3 commits into
mainfrom
farm/dc26a4ab/fix-codeframe-no-trailing-newline

Conversation

@robobun

@robobun robobun commented Aug 1, 2026 •

Copy link
Copy Markdown
Collaborator

When a source file or bun -e input has no trailing newline and an error is thrown on the last line, the code-frame preview labels lines off by one and never shows the throwing line.

Repro

$ bun -e $'const a = 1;\nconst b = 2;\nthrow new Error("x");'
2 | const a = 1;
3 | const b = 2;
              ^
error: x
      at [eval]:3:11

The stack frame [eval]:3:11 is correct; only the preview is wrong. Appending a trailing \n to the input makes the preview correct. The same happens for files without a trailing newline and for Bun.inspect(new Error(...)) when the new Error is on the last line.

Cause

index_of_line_ranges scans text and pushes a LineRange each time it crosses a newline. When the loop runs out of newlines it falls through and returns, so the final unterminated line is never pushed. remap_zig_exception assumes the last returned range is target_line and numbers backward from there, so every label is off by one and the target line's content is missing.

Fix

After the scan loop, if there is content past the last newline (prev_end + 1 < text.len()), push {prev_end, text.len()} as the final range, making room in the sliding window first. The old after-loop block only made room but never pushed.

After

$ bun -e $'const a = 1;\nconst b = 2;\nthrow new Error("x");'
1 | const a = 1;
2 | const b = 2;
3 | throw new Error("x");
              ^
error: x
      at [eval]:3:11

Verified for -e/--print, files, CRLF, non-ASCII on the final line, and inputs longer than the preview window. Tests in run-eval.test.ts cover -e/--print, a file, and Bun.inspect.


no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/run/run-eval.test.ts

When the source text has no trailing newline and the target line is the
last (unterminated) line, index_of_line_ranges would exit its scan loop
without ever pushing that line's range. remap_zig_exception assumes the
last returned range is the target line and numbers backward from there,
so the code frame showed the wrong line numbers and omitted the line
that actually threw.
@coderabbitai

coderabbitai Bot commented Aug 1, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The line-range index now includes non-empty unterminated final lines. CLI tests cover error code frames and Bun.inspect(new Error(...)) output for eval and file execution without trailing newlines.

Changes

Final-line error reporting

Layer / File(s) Summary
Index unterminated final lines
src/bun_core/string/immutable.rs
index_of_line_ranges records the non-empty text tail after the last newline and maintains the bounded range list.
Validate eval and file error output
test/cli/run/run-eval.test.ts
Tests validate final-line code frames, source locations, stdout/stderr output, and exit codes for eval and file execution without trailing newlines.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main fix: including an unterminated final line in error code-frame previews.
Description check ✅ Passed The description explains the problem, cause, fix, affected cases, reproduction, and verification coverage, although it does not use the template headings.

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

@github-actions github-actions Bot added the claude label Aug 1, 2026
@robobun

robobun commented Aug 1, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 3:59 AM PT - Aug 1st, 2026

⏳ @robobun, your commit 29fd565 is still building in Build #87100, but has 2 failures so far (All Failures):

@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.

LGTM — small, well-scoped fix to the error code-frame preview. The one inline nit (undrained stdout pipe) is cosmetic; the child writes nothing to stdout so there's no deadlock risk.

Checked both callers of index_of_line_ranges (VirtualMachine.rs remap path and css/error.rs) — both assume the last returned range is target_line and number backward, so both benefit. Traced the trailing-newline case: prev_end + 1 == text.len() leaves behavior unchanged. Dropping the old current_line <= target_line guard is safe since that invariant always holds at the fall-through point.

Extended reasoning...

Overview

Fixes an off-by-one in the error code-frame preview when source has no trailing newline. The change is 11 net lines in index_of_line_ranges (src/bun_core/string/immutable.rs): after the newline-scan loop exits, if there is content past the last \n, push it as the final LineRange. The old code only made room in the sliding window without pushing anything. Four new tests in run-eval.test.ts cover -e, --print, running a file, and Bun.inspect(new Error(...)).

Security risks

None. This is display-only formatting for error previews; it reads bytes from source text already in memory and produces line-range indices. No parsing of untrusted input beyond what already existed, no allocation changes, no FFI.

Level of scrutiny

Low-to-medium. The function is not on a hot path (only runs when formatting an error's code frame) and has exactly two callers, both of which I checked:

  • VirtualMachine.rs:5469 — numbers lines backward from target_line, assuming the last returned range is that line. The fix makes this assumption hold for unterminated final lines.
  • css/error.rs:185 — fetches a single line for CSS diagnostics; same assumption, same benefit.

I traced the invariants: at the fall-through point current_line <= target_line always holds (the loop returns early otherwise), so dropping that condition from the make-room block is correct. For inputs that do end with \n, prev_end + 1 == text.len() and the new block is skipped, preserving prior behavior. The pushed range's start: prev_end (index of the last \n) matches the leading-newline convention used by the in-loop ranges. The as u32 wrapping cast follows the file's existing pattern.

Other factors

Tests follow REVIEW.md conventions (spawnSync + stderr assertions before exitCode, tempDir, test.concurrent for the async subprocess pair) and cover the variant matrix: both eval flags, file execution, and Bun.inspect. The one nit (stdout piped but not drained in one test) is harmless here since the child writes 0 bytes to stdout — worth fixing for consistency with the sibling test but not blocking. No CODEOWNERS on these paths, no outstanding reviewer comments.

Comment thread test/cli/run/run-eval.test.ts Outdated
Comment thread src/bun_core/string/immutable.rs Outdated

@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: 2

🤖 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/bun_core/string/immutable.rs`:
- Around line 1733-1736: Guard both line-range eviction conditions, including
the block around new_ranges, with LINE_RANGE_COUNT != 0 before slicing from
index 1; preserve existing eviction behavior for nonzero capacities. Add
regression coverage for zero-capacity handling of inputs such as b"a\nbc" and
b"a\nb\nc".

In `@test/cli/run/run-eval.test.ts`:
- Around line 51-61: Update test/cli/run/run-eval.test.ts at lines 51-61,
141-146, and 161-166 to assert complete normalized code-frame output in the
eval, file-error, and Bun.inspect() tests. Pipe and verify empty stdout in the
eval test, replace independent toContain checks with one ordered exact frame
assertion per test, and run bun bd test test/cli/run/run-eval.test.ts.
🪄 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: 5af85acf-3537-4721-9ed6-d995274109dc

📥 Commits

Reviewing files that changed from the base of the PR and between a7838c5 and 4d49d53.

📒 Files selected for processing (2)
  • src/bun_core/string/immutable.rs
  • test/cli/run/run-eval.test.ts

Comment thread src/bun_core/string/immutable.rs
Comment thread test/cli/run/run-eval.test.ts Outdated
Comment thread src/bun_core/string/immutable.rs
…, window-full test

The bare-CR arm of the scan loop pushed a range but left prev_end stale
(and computed the range end from the advanced cursor, not the CR
position). Update prev_end and the end index so the tail-push guard
does not re-push an overlapping range for sources that mix LF and bare
CR.

Tests now assert the code-frame lines as one ordered block and assert
empty stdout, and a new 8-line case covers the full-window shift
branch.

@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.

LGTM — focused fix for the off-by-one code-frame preview when the target line is unterminated.

What was reviewed:

  • Traced index_of_line_ranges tail push against both callers (VirtualMachine.rs numbers backward from last_line; css/error.rs with LINE_RANGE_COUNT=1) — both now get the correct final range.
  • Verified the bare-CR else-arm change (current_end + 1 + prev_end = current_end) fixes the overshoot when the peek advances the cursor, and is a no-op when the peek hits EOF.
  • Confirmed the dropped && current_line <= target_line guard was always true at that point (early return inside the loop guarantees it).
  • Tests drain both pipes, assert the frame as one ordered block, cover -e/--print/file/Bun.inspect, and exercise the window-shift path.
Extended reasoning...

Overview

The PR fixes index_of_line_ranges in src/bun_core/string/immutable.rs so the unterminated final line of a source is included in the returned range window. Previously the scan loop pushed a range per newline and fell through without pushing the tail, so remap_zig_exception (which numbers backward from the last returned range assuming it is target_line) mislabeled every preview line by one and omitted the throwing line whenever the source lacked a trailing newline. A secondary fix in the same function updates prev_end in the bare-CR branch and corrects its range end from cursor.i + 1 (which overshoots by one when the peek advances) to current_end + 1. Four tests are added to test/cli/run/run-eval.test.ts.

Security risks

None. This is a crate-private helper feeding a cosmetic error-preview renderer. Ranges are bounded by text.len(); the only new arithmetic is (prev_end as usize) + 1 < text.len(), which cannot overflow (prev_end is a valid byte index into text). No user-controlled sizing or allocation.

Level of scrutiny

Low-medium. The change is ~15 net lines in a function with exactly two callers. I traced both:

  • VirtualMachine.rs:5469 (SOURCE_LINES_COUNT = 6): iterates the reversed result assigning last_line, last_line-1, ... — now correctly aligned since the last range is actually target_line.
  • css/error.rs:185 (LINE_RANGE_COUNT = 1): takes [0] after reversal — previously got line N-1 when the target was the unterminated line N; now gets N.

The dropped current_line <= target_line guard on the post-loop shift was dead: the in-loop if current_line >= target_line { return } runs before the increment, so falling out of the loop implies current_line <= target_line.

Other factors

Two rounds of review feedback were addressed (stdout draining in the file test; the bare-CR/prev_end interaction I flagged; comment length; ordered-block assertions per coderabbit). All threads are resolved. Tests follow harness conventions (tempDir, bunEnv, concurrent subprocess drain, exitCode asserted last) and include a window-full case that exercises the ranges.len() == LINE_RANGE_COUNT shift. The bare-CR change is a strict improvement over pre-PR behavior (which never updated prev_end in that arm) and does not affect the first-newline block, which handles CR-only single-newline inputs separately.

@robobun

robobun commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

CI on 29fd565: the new tests in test/cli/run/run-eval.test.ts are not in the failure list on any lane. The red lanes are unrelated to this change:

  • [new] bun-upgrade.test.ts on Windows aarch64 ("Canary builds are not available for this platform yet"), a release-artifact availability issue
  • [new] worker-transfer-terminate-stress.test.ts SIGABRT on the ASAN lane (JSC ExceptionScope assertion in worker terminate), unrelated to the string line-range helper
  • everything else is tagged [flaky] and passed on retry or when run alone (request-clone-leak, test-fastutf8stream-reopen, bun-install-registry, 23865, tty-reopen-after-stdin-eof, test-error-code-done-callback, inspect-error-leak)

This diff is ready for review; all review threads are resolved.

This branch has not been deployed

No deployments
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.

1 participant