Skip to content

logger: indent the caret relative to the windowed line excerpt - #41658

Open
robobun wants to merge 10 commits into
mainfrom
robobun/9216d528/caret-under-windowed-excerpt
Open

robobun wants to merge 10 commits into
mainfrom
robobun/9216d528/caret-under-windowed-excerpt

Conversation

@robobun

@robobun robobun commented Sep 6, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • The logger cuts a long source line to about 120 bytes around the error, but pads the caret to the error's column in the whole line. A one-line bun.lock with 1000 bad integrity hashes (warn: Unsupported or malformed integrity hash; ignoring) prints 22 MB of stderr.
  • The cause is Data::write_format in src/ast/lib.rs. It indents the caret by column - 1. Location::init_or_null_impl drops bytes from the left of line_text and records nothing about it.
  • A line after a U+2028 or U+2029 terminator was excerpted from one byte early, so a stray continuation byte was printed.

Fix

  • Location gets line_text_start_column: the column at which line_text starts in the source line. write_format subtracts it from the caret indent and bounds the indent by the excerpt length.
  • The reported column and the at file:line:col suffix still refer to the whole line.
  • The excerpt starts at the line's first byte.
  • Verified: test/cli/install/bun-lock.test.ts (one new test) and test/js/bun/transpiler/parse-error-column.test.ts (seven new tests). All fail on the release build and pass here.

Background

Notes

The window is up to 40 bytes before the error and 80 after, snapped to UTF-8 boundaries. An error in the last 80 bytes of a line keeps the whole line, because the bake overlay and BuildMessage.position index line_text by column alone. LineColumnTracker finds line and column for many diagnostics in one file without rescanning from the top, so only the kept bytes before the error are measured, not the dropped prefix. The trim_left of \n\r that covered for the early line start is gone. On the release build, the longest caret line for N=1000 is 43,800 chars.

Repro: a bun.lock whose packages object is one line with N entries "pK": ["pK@1.0.0", "", {}, "sha512-x"], package.json with no dependencies, then bun install.

  • Release 1.4.3-canary.1, N=1000: 22,112,179 bytes of stderr, longest line 43,800 chars, 102 s in this container. N=300: 2,020,023 bytes.
  • This branch (debug build), N=300: 126,375 bytes. The few warnings in the last 80 bytes of the line still print the whole line, so the longest line is the lockfile line itself, printed a handful of times instead of N times.

Before the fix the first warning on the repro line (column 44) already showed the caret 4 cells right of the token, because the window started at byte 4 of the line.

Location.length is now u32 and the new field is u32. A usize field grew bun_ast::Msg from 152 to 160 bytes, which put LoadValue::Err(Msg) in src/bundler/bundle_v2.rs over clippy's large_enum_variant threshold (128). Both values come from i32 ranges, so Location, Data and Msg keep their size (96, 120, 152 bytes).

A first revision also windowed errors in the last 80 bytes of a long line. Review pointed out that serialized_failure.rs / overlay.ts, DevErrorPage.rs and BuildMessage.position expose lineText with the full-line column and no start column, so that widening would have misplaced their highlight for tail-of-line errors. It was dropped; carrying the offset through those consumers is #37872's scope.

Self-reviewed: 2 concerns raised (the widening above, and the dropped non-ASCII/astral/error+note tests from #37848), both addressed.

Suites run with the debug build: test/cli/install/bun-lock.test.ts, test/js/bun/transpiler/parse-error-column.test.ts (14 pass), test/bundler/bundler_edgecase.test.ts (170 pass, DeepImportDiamondDAG timed out at 120 s under ASAN in this container, unrelated), test/js/bun/test/dots.test.ts, test/js/bun/test/only-failures.test.ts, test/regression/issue/12782.test.ts.

The ledger note also suggests collapsing N identical warnings into one with a count. This PR does not do that.


no test proof · iteration 4 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/cli/install/bun-lock.test.ts

The excerpt of a long source line is cut to about 120 bytes around the
error, but the caret under it was padded to the error's column in the
whole line. A one-line bun.lock with N bad integrity hashes printed N
caret lines of up to the line's width, so stderr grew quadratically
(22 MB for 1000 entries) and the caret never pointed at the excerpt.

Location records how many columns the window drops on the left, and
the printer subtracts that from the column and bounds the indent by the
excerpt length. An error in the last 80 bytes of a long line now gets
the window too, instead of printing the whole line.
@coderabbitai

coderabbitai Bot commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review paused — included plan limit reached

Keep your review moving with free on-demand reviews.

  • Run this review for free

On-demand reviews are free for one more day.

  • Ask an admin to make reviews automatic

Open in CodeRabbit

Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing.

Promotion and pricing details

On-demand reviews are free for one more day. After that, they cost $0.25 per reviewed file.

Review limit details

Or wait 1 minute for your next included review.

Check out review usage here.

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: oven-sh/bun/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 3c468bd4-4eea-4d2f-b572-cef859c5c1ac

📥 Commits

Reviewing files that changed from the base of the PR and between 9b7c982 and 7be9837.

📒 Files selected for processing (5)
  • src/ast/lib.rs
  • src/parsers/json.rs
  • src/runtime/bake/dev_server/serialized_failure.rs
  • test/cli/install/bun-lock.test.ts
  • test/js/bun/transpiler/parse-error-column.test.ts

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

@github-actions github-actions Bot added the claude label Sep 6, 2026
@robobun

robobun commented Sep 6, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 8:18 PM PT - Sep 19th, 2026

❌ @robobun, your commit 7be9837 has 1 failures in Build #118787 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 41658

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

bun-41658 --bun

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

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment thread src/ast/lib.rs Outdated
Comment thread test/cli/install/bun-lock.test.ts Outdated
…e start after U+2028

Drop the widened window gate: consumers that only know the column (the
bake overlay, BuildMessage.position) still get the full line for an
error in its last 80 bytes. Count the kept prefix with the same scanner
that produced the column. Start the excerpt at the line's first byte so
a line after a U+2028/U+2029 terminator does not begin with a stray
continuation byte. Port the left-trim, non-ASCII, astral, error+note,
package.json and U+2028 caret tests from #37848. Drain stdout in the
lockfile test and assert the output size is linear.
Comment thread src/ast/lib.rs Outdated
Comment thread src/ast/lib.rs Outdated
Comment thread src/ast/lib.rs Outdated
Comment thread src/ast/lib.rs Outdated
Comment thread src/ast/lib.rs Outdated
Comment thread src/ast/lib.rs Outdated
Comment thread src/ast/lib.rs Outdated
Comment thread src/ast/lib.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.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Both earlier concerns are addressed in the latest push: the || full_line.len() > 120 widening of the window gate is reverted (with a comment naming the bake overlay and BuildMessage.position as consumers that still index the excerpt by column - 1), and the bun-lock test now drains stdout alongside stderr/exited. The test's stderr-length bound was correspondingly relaxed to account for the last-80-bytes entries still printing the full line — checked that this stays linear in the entry count.

Extended reasoning...

The follow-up commit reverts the tail-of-line window widening that the previous review flagged as regressing other Location consumers, and instead documents the constraint in a comment; it also fixes the undrained stdout pipe. The kept-bytes column count now reuses ErrorPositionState::advance (the same scanner that produced column_count), and the line_start - 1 back-up plus the trim_left(full_line, b"\n\r") are both removed together, which is the U+2028 continuation-byte fix covered by the new test. The remaining inline finding is a Windows CRLF nit in the new bun-lock test.

Comment thread test/cli/install/bun-lock.test.ts 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.

LGTM — all three points from earlier rounds are addressed: the || full_line.len() > 120 widening is gone (so the bake overlay / BuildMessage.position consumers keep seeing the full line for tail-of-line errors), the bun-lock test now drains stdout concurrently, and both new tests split stderr on /\r?\n/.

Checked that line_text_start_column is threaded through every Location constructor/clone site (Clone, Default, dupe, init_or_zero, both init_or_null_impl branches); the line_start - 1 removal and the dropped trim_left on full_line are paired and covered by the U+2028 test; the line_column_tracker_tests unit tests compare tracker vs. full-scan through the same to_error_position, so they stay consistent. bunEnv already sets NO_COLOR: "1", so dropping the explicit override in the refactored helper is fine.

Extended reasoning...

Overview

This PR fixes caret misalignment in Bun's diagnostic printer when a long source line is windowed to ~120 bytes. It adds line_text_start_column: usize to ast::Location to record how many UTF-16 column units were dropped from the left of the excerpt, and Data::write_format subtracts that offset (clamped to the excerpt length) when indenting the ^. ErrorPositionState::to_error_position no longer subtracts 1 from line_start, and the corresponding trim_left(full_line, b"\n\r") on the stored line_text is removed — this also fixes a stray UTF-8 continuation byte after U+2028/U+2029. Two test files are extended: parse-error-column.test.ts gains a printedExcerpts helper and seven cases (ASCII/2-byte/astral prefixes, error+note, bun install on long package.json, U+2028), and bun-lock.test.ts gains a 300-warning single-line lockfile test asserting caret placement and linear stderr growth.

Security risks

None. This is diagnostic-output formatting only — no parsing of untrusted data into privileged operations, no auth/crypto/permissions, no network. The only arithmetic on user-derived values uses saturating_sub and .min(), so no overflow/underflow on adversarial column counts.

Level of scrutiny

Medium — shared diagnostic infrastructure with several downstream consumers (bake overlay, BuildMessage.position, DevErrorPage). The first revision's widened windowing gate would have regressed those consumers for tail-of-line errors; that was reverted after review, and the current gate is byte-identical to base (full_line.len() > 80 + offset_in_line), so those readers see unchanged line_text with line_text_start_column == 0. The new field is wired through every constructor/clone site I could find. The to_error_position change is validated by the new U+2028 test, and the in-file line_column_tracker_tests compare two paths that both go through the changed function, so they remain self-consistent.

Other factors

This is my fourth pass. All three prior findings (the widened gate, undrained stdout in the bun-lock test, and "\n" vs /\r?\n/ splitting) are fixed in the current diff. The tests follow harness conventions (tempDir, bunExe(), bunEnv, concurrent pipe draining, test.concurrent, Buffer.alloc(n, fill) instead of .repeat, /\r?\n/ splitting, exit-code asserted last). bunEnv in test/harness.ts already sets NO_COLOR: "1", so dropping the explicit { ...bunEnv, NO_COLOR: "1" } in the refactored helper is a no-op. No open CHANGES_REQUESTED from human reviewers; the author-resolved github-actions[bot] threads land on lines that changed shape across the intervening commits, consistent with them being addressed. The exit reason is dry_streak.

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

Code review found no issues

No high-confidence issues detected in this change.

The new usize field grew bun_ast::Msg from 152 to 160 bytes, which put
LoadValue::Err(Msg) in the bundler over clippy's large_enum_variant
threshold. Store the start column and the highlight length as u32 (both
come from i32 ranges), so Location, Data and Msg keep their size.
Comment thread src/ast/lib.rs

@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 — the follow-up commits since my last pass (the usize → u32 narrowing of Location.length and the new line_text_start_column field) are consistent across every reader and constructor I could find.

What was reviewed:

  • Location.length readers: src/parsers/json.rs:154 (cast to usize before the add), src/runtime/bake/dev_server/serialized_failure.rs:232 (writes the u32 directly), src/jsc/BuildMessage.rs:152 (From<u32> for JSValue exists) — no other field reads found; all other Location literals use ..Default::default().
  • line_text_start_column derivation in init_or_null_impl: the kept-prefix counter runs the same ErrorPositionState::advance over full_line[lo..offset_in_line] as the full-line column scan, so a truncated UTF-8 tail or astral characters count identically in both and the difference is exact in UTF-16 units; the window gate is unchanged, so bake's overlay and BuildMessage.position see the same excerpts as base.
  • Dropping the -1 in to_error_position: line_start is set after the terminator in every branch of advance, and the tracked and from-scratch scans share the same conversion, so the existing LineColumnTracker equivalence tests still cover both; for \n/\r\n inputs the result matches the old trim_left output byte-for-byte, and the U+2028 case is now correct and tested.
Extended reasoning...

Overview

The PR fixes caret placement under the logger's windowed source excerpts in src/ast/lib.rs: Location gains line_text_start_column: u32 (0-based, UTF-16 units) recording where a left-trimmed line_text begins, Data::write_format subtracts it from the caret indent (clamped to the excerpt length), and ErrorPositionState::to_error_position no longer points line_start one byte before the line (which after a 3-byte U+2028 was a stray continuation byte). length is narrowed from usize to u32 to keep Location/Data/Msg at their prior sizes; the two callers that read it (src/parsers/json.rs, src/runtime/bake/dev_server/serialized_failure.rs) are adapted. Tests: a new printedExcerpts helper in test/js/bun/transpiler/parse-error-column.test.ts with cases for left-trimmed excerpts under build and run, 2-byte and 4-byte UTF-8 prefixes, error + note on one long line, bun install on a long package.json line, and the U+2028 line start; plus a bun-lock.test.ts case asserting 300 carets land under "sha512- in the excerpt and that stderr stays linear in the entry count.

Security risks

None identified. The change only affects how diagnostics are rendered to stderr and the values carried in Location. The new arithmetic is bounded: column_count - kept.column_number uses saturating_sub and both operands are UTF-16 unit counts over a source that is at most i32::MAX bytes, so the u32::try_from(...).expect cannot trip on user input; the caret indent is clamped to line_text.len(). The windowing slice indices (lo, hi) are unchanged from base. No allocation, parsing, or trust-boundary changes.

Level of scrutiny

Moderate. The struct is shared by the bundler, BuildMessage, and the bake dev server, so I enumerated every reader of Location.length and every Location literal in src/ (all others use ..Default::default()), and traced ErrorPosition.line_start to its only two uses (the excerpt slice and offset_in_line). I also checked that the window gate (full_line.len() > 80 + offset_in_line) is unchanged, so consumers that index line_text by the full-line column (bake overlay, BuildMessage.position) receive exactly the excerpts they did on base; the PR explicitly scopes carrying the offset through those consumers to a separate PR. I could not run cargo check or the test suites in this environment, so compile verification rests on the grep audit and CI.

Other factors

The exit reason was dry_streak, no bugs were reported, and the changed files are not covered by CODEOWNERS. My prior reviews raised two items (pipe draining, CRLF split) that are now fixed in the diff (Promise.all over stdout/stderr/exited, split(/\r?\n/)), and the earlier tail-of-line widening concern was dropped by the author. The one outstanding github-actions[bot] inline comment at src/ast/lib.rs:650 was followed four minutes later by the "keep Location.length in place" commit that touches exactly that region, which plausibly addressed it. The tests are specific (toEqual on exact excerpt/caret pairs, Buffer.alloc fills, test.concurrent, hermetic tempDir), and each load-bearing clause of the fix has a test that would fail without it (UTF-16 vs byte vs character counting are distinguished by the astral and U+00E9 cases).

@robobun

robobun commented Sep 19, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review. The diff is green. CI is red on one test that also fails on main.

Reproduction: write a bun.lock whose packages object is one line with 1000 entries "pK": ["pK@1.0.0", "", {}, "sha512-x"], add a package.json with no dependencies, and run bun install. The release build prints 22 MB of stderr, with caret lines of up to 43,800 spaces. On this branch each caret sits under its excerpt and the output grows linearly with the entry count.

CI on 7be9837 (build 118787), a rerun of the same diff:

  • cargo clippy passes. No failure is in a test that this PR adds or changes.
  • One job is red: test/js/bun/spawn/spawn.test.ts on debian 13 x64-asan (stdout reader of an unref'd child and process lifetime). The same test fails on main.
  • The five x64-asan failures of build 118690 (four leak-test timeouts and vendor/elysia/test/response/stream.test.ts) did not recur.
  • test/cli/install/bun-lock.test.ts passed on retry. The test that flaked is the existing peer no published version satisfies (hoisted linker) > declared by a registry package (line 1819), which flakes the same way on unrelated branches (builds 118678, 118671, 118656, 118653, 118629).

This diff only changes how the logger prints diagnostics, so it does not reach any of those code paths. The build finished with 180 of 181 jobs passed.

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.

2 participants