Conversation
…frames index_of_line_ranges ended the first line's range at the '\n' of a CRLF pair while every later line's range ends at the '\r', so line 1 of a CRLF file was the only line printed with a trailing carriage return in the code frame above an error, in Bun.inspect(err), and in the lineText of CSS diagnostics reported on line 1.
|
Updated 1:05 AM PT - Aug 14th, 2026
❌ @robobun, your commit 9ad41a7 has some failures in 🧪 To try this PR locally: bunx bun-pr 38338That installs a local version of the PR into your bun-38338 --bun |
|
Status: reproduced on the released binary and on a debug build of main ( |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughCRLF line-range scanning now preserves carriage-return positions and resumes later scans from the newline cursor. New tests cover JavaScript and CSS diagnostics, error inspection, empty lines, normalized frames, and exit codes. ChangesCRLF line-range handling
Suggested reviewers: Merge Risk: ⚪ Minimal · up to This removes an unintended carriage return from first-line CRLF diagnostics and code frames while preserving existing behavior for other line endings and consumers; no actionable merge-blocking risk remains beyond normal checks and review. 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
LGTM — targeted fix that ends line 1's range at the \r of a \r\n pair, matching what the main loop already does for lines 2+.
What was reviewed:
- Traced
prev_end = cursor.ifor both the LF arm (cursor.i == first_newline_range.end, unchanged) and the CRLF arm (still the\nindex, so line 2's start is unchanged andtrimmed_text()still strips it). - Confirmed
trimmed_text()inZigStackTrace.rs:129trims only\n, so the range-level fix is the right layer; checked callers (VirtualMachine.rs,css/error.rs). - Checked the bare-
\r-at-EOF and\r-not-followed-by-\npaths in the first-line scan — both fall through unchanged (they don't reachprev_end). - Tests cover error below/on line 1, empty line 1,
Bun.inspect, and the CSSlineTextconsumer; harness conventions (tempDir, bunEnv, concurrent pipe drain,describe.concurrent) look right.
Extended reasoning...
Overview
Two-line logic change in index_of_line_ranges (src/bun_core/string/immutable.rs): the first-line scan now ends the range at the \r of a \r\n pair (via a saved cr_i) instead of at the \n, and prev_end is initialized from cursor.i rather than first_newline_range.end so the main loop's resume point is unchanged. A new test file test/js/bun/util/inspect-error-crlf.test.ts adds 5 subprocess/build tests.
Security risks
None. This affects only the byte range boundaries used to slice source text for error code frames and diagnostic lineText. No parsing of untrusted input, no allocation-size arithmetic, no auth/crypto surface.
Level of scrutiny
Low-to-moderate. The helper is used only for diagnostic output (error code frames, CSS position.lineText, inspector sourceLines), so a mistake would degrade error messages, not runtime behavior. The change is mechanical: I walked each arm of the first-line 'brk block and confirmed that for the NL case cursor.i == first_newline_range.end (so prev_end is byte-identical to before), and for the CRLF case cursor.i is still the \n index (so prev_end is byte-identical to before). The only observable change is first_newline_range.end moving one byte left on CRLF, which is exactly the bug fix. The paths that return before reaching prev_end (no-newline fallback, bare \r at EOF) don't touch the new code.
Other factors
- Fix is at the layer that owns the invariant (the range producer) rather than patching each consumer, and it makes line 1 consistent with the existing lines-2+ behavior in the same function.
- The PR description explains why
prev_endmust stay at the\n(sotrimmed_text()'s\n-only trim keeps working for line 2's leading char); I verifiedtrimmed_text()atsrc/jsc/ZigStackTrace.rs:129-131and it indeed trims only\n. - Tests follow harness conventions:
tempDir/bunEnv/bunExe,describe.concurrent, concurrentPromise.alldrain of stdout/stderr/exited, exact.toEqualassertions on the combined{stdout, stderr, exitCode}shape. The separate-file justification (inspect-error.test.js pins its own line numbers in inline snapshots) is sound. - The PR states all 5 tests fail on the released binary and on debug main without the src/ change, and pass with it; and that adjacent suites (
inspect-error.test.js,reportError.test.ts, CSS error tests) still pass.
Problem
Bun.inspect(err)/console.error(err)output) from a file with CRLF line endings, line 1 keeps its carriage return:1 | 'L1';\r. Lines 2 and later print cleanly. Only visible when line 1 is inside the preview window (the error is on one of the first 6 lines).\rshows up asposition.lineText("}\r") of a CSS diagnostic reported on line 1 of a CRLF stylesheet, since the CSS parser locates its diagnostics with the same helper.index_of_line_rangesinsrc/bun_core/string/immutable.rs. The scan for the first line (line 1799 on main) ends the range at the\nof a\r\npair, so the\rstays inside it. The main loop (line 1842 on main) ends every later line at the\r, which is why only line 1 is affected. The display helper (trimmed_text()insrc/jsc/ZigStackTrace.rs) strips\nonly, and the inspector'sLifecycleReporter.errorpayload andBuildMessage.position.lineTextuse the ranges as they are.Fix
\rwhen the first line break is\r\n, the same boundary the main loop already uses for every other line.\n(prev_end = cursor.i, which is the\nin both the\nand\r\ncases, so the main loop is unchanged). Later ranges deliberately start at the\nthat ended the previous line becausetrimmed_text()strips\nfrom both ends; starting them at the\rinstead would put\r\nat the front of line 2, whichtrimmed_text()does not strip.\rtotrimmed_text()fixes every consumer of the helper at once (code frame,Bun.inspect, CSSlineText, inspector payload) and matches what the helper already does for lines 2+. LF files are unaffected: their first range already ended at the\nexclusive, andprev_endhas the same value as before.ZigException.cpp, handled in error printer: fix the code frame lines above errors thrown from vm/eval sources #38244) and is not affected either way.test/js/bun/util/inspect-error-crlf.test.ts: uncaught error below line 1, on line 1 (thetarget_line == 0early return), empty line 1,Bun.inspect(err), and a CSS diagnostic on line 1. All 5 fail on the released binary and on a debug build of main withsrc/stashed; all pass with the fix. The existinginspect-error.test.js,reportError.test.ts,test-test.test.ts,run-eval.test.ts, and the CSS error tests still pass. These tests are a sibling file becauseinspect-error.test.jspins its own line numbers in inline snapshots, so adding an import there rewrites unrelated snapshots.Background
name: messagebun prints above a stack trace. For transpiled filesremap_zig_exception(src/jsc/VirtualMachine.rs) re-reads the original file and callsget_lines_in_text, which wrapsindex_of_line_ranges, to cut out the error line and up to 5 lines above it.index_of_line_rangesreturns one(start, end)byte range per line. It scans the first line separately from the rest: the first range starts at byte 0, and each later range starts where the previous line's terminator was found (prev_end), so lines 2+ carry a leading\nthat the printers trim (trimmed_text()inZigStackTrace.rs, and the logger's own trimming for CSS diagnostics printed by bun itself).InspectorLifecycleAgent.cppsends the strings to debugger frontends assourceLines, andBuildMessage.rsexposes a CSS diagnostic's line asposition.lineText.Repro
Before:
After:
CSS, before and after (
printf '}\r\na {}\r\n' > a.css, thenBun.build({ entrypoints: ["./a.css"], throw: false })):logs[0].position.lineTextis"}\r"before and"}"after. Diagnostics on lines 2+ of a CSS file still carry the leading\ndescribed above; that is a separate shape in the CSS consumer and is not changed here.