From d8257713986536f2dcdeb58225f072fc6b981509 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 17 Sep 2026 23:33:55 +0000 Subject: [PATCH 1/5] logger: bound the excerpt printed for an error on a long line Data::write_format printed Location.line_text in full and indented the caret by column - 1. A location keeps the whole line for an error in the last 80 bytes of it, and a CSS location always keeps the whole line, so a syntax error at the end of a 400 KB one-line file printed 800 KB. write_format now prints at most 120 bytes of the line around the caret and indents the caret by what it printed. line_text and column do not change. --- src/ast/lib.rs | 61 ++++++- .../bun/transpiler/parse-error-column.test.ts | 152 +++++++++++++++++- 2 files changed, 204 insertions(+), 9 deletions(-) diff --git a/src/ast/lib.rs b/src/ast/lib.rs index 8f450860cfc8..fbe7b6d0b401 100644 --- a/src/ast/lib.rs +++ b/src/ast/lib.rs @@ -978,11 +978,13 @@ impl Data { if let Some(location) = &self.location { if let Some(line_text_) = location.line_text.as_deref() { - let line_text_right_trimmed = bun_core::trim_right(line_text_, b" \r\n\t"); - let line_text = bun_core::trim_left(line_text_right_trimmed, b"\n\r"); - if location.column > 0 && !line_text.is_empty() { - let mut line_offset_for_second_line: usize = - usize::try_from(location.column - 1).expect("int cast"); + let line_text = bun_core::trim_left(line_text_, b"\n\r"); + if location.column > 0 && !bun_core::trim_right(line_text, b" \r\n\t").is_empty() { + let (line_text, caret_column) = excerpt_around_column( + line_text, + usize::try_from(location.column - 1).expect("int cast"), + ); + let mut line_offset_for_second_line: usize = caret_column; if location.line > -1 { let bold = matches!(kind, Kind::Err | Kind::Warn); @@ -1071,6 +1073,55 @@ fn write_n_bytes(to: &mut impl fmt::Write, b: u8, n: usize) -> fmt::Result { Ok(()) } +/// `Data::write_format` prints at most this many bytes of a line, plus the +/// bytes that complete a UTF-8 sequence at either end. +const MAX_EXCERPT_LEN: usize = 120; +/// The excerpt starts this many bytes before the caret, or earlier when the +/// line ends before the excerpt is full. +const EXCERPT_LEN_BEFORE_CARET: usize = 40; + +/// Walks `text` until `columns` UTF-16 code units, the unit of +/// `Location::column`, have gone by or `text` ends. Returns the bytes and the +/// code units walked. +fn advance_utf16_columns(text: &[u8], columns: usize) -> (usize, usize) { + use bun_core::strings::{CodepointIterator, Cursor}; + let iter = CodepointIterator::init(text); + let mut cursor = Cursor::default(); + let mut walked: usize = 0; + while walked < columns && iter.next(&mut cursor) { + walked += 1 + (cursor.c > 0xFFFF) as usize; + } + (cursor.i as usize + cursor.width as usize, walked) +} + +/// The part of `line_text` that `Data::write_format` prints for a caret at the +/// 0-based `column`, and the caret's column in that part. Not every producer of +/// a `Location` bounds `line_text`, and `column` can lie past its end. +fn excerpt_around_column(line_text: &[u8], column: usize) -> (&[u8], usize) { + let (caret, caret_column) = advance_utf16_columns(line_text, column); + let visible = bun_core::trim_right(line_text, b" \r\n\t"); + if line_text.len() <= MAX_EXCERPT_LEN { + return (visible, caret_column); + } + let caret = caret.min(visible.len()); + let mut hi = + (caret.saturating_sub(EXCERPT_LEN_BEFORE_CARET) + MAX_EXCERPT_LEN).min(visible.len()); + let mut lo = hi.saturating_sub(MAX_EXCERPT_LEN); + // A UTF-8 sequence has at most 3 continuation bytes. + for _ in 0..3 { + if !bun_core::strings::is_on_char_boundary(visible, lo) { + lo -= 1; + } + if !bun_core::strings::is_on_char_boundary(visible, hi) { + hi += 1; + } + } + ( + bun_core::trim_right(&visible[lo..hi], b" \r\n\t"), + advance_utf16_columns(&visible[lo..caret], usize::MAX).1, + ) +} + // ─────────────────────────────────────────────────────────────────────────── // BabyString // ─────────────────────────────────────────────────────────────────────────── diff --git a/test/js/bun/transpiler/parse-error-column.test.ts b/test/js/bun/transpiler/parse-error-column.test.ts index 796f1b3cf6e8..a2d9167d291e 100644 --- a/test/js/bun/transpiler/parse-error-column.test.ts +++ b/test/js/bun/transpiler/parse-error-column.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "bun:test"; +import { describe, expect, test } from "bun:test"; import { bunEnv, bunExe, tempDir } from "harness"; import { join } from "node:path"; @@ -142,8 +142,8 @@ test.concurrent("long non-ASCII line's lineText window does not split a UTF-8 se }); test.concurrent("CLI caret stays under the token for an error at the end of a long line", async () => { - // `write_format` offsets the caret by `column - 1` with no knowledge of any - // left-trim, so the window gate must not left-trim this case. + // The location keeps the whole 151-byte line. The printer shows its last 120 + // bytes and indents the caret by what it printed, not by the column. using dir = tempDir("parse-col-caret", { "long.js": Buffer.alloc(150, "a").toString() + "]", }); @@ -158,5 +158,149 @@ test.concurrent("CLI caret stays under the token for an error at the end of a lo const lines = stderr.split("\n"); const textLine = lines.find(l => l.includes("]"))!; const caretLine = lines.find(l => l.trimEnd().endsWith("^"))!; - expect({ token: textLine.indexOf("]"), caret: caretLine.indexOf("^") }).toEqual({ token: 154, caret: 154 }); + expect({ token: textLine.indexOf("]"), caret: caretLine.indexOf("^") }).toEqual({ token: 123, caret: 123 }); +}); + +const fill = (count: number, char: string) => Buffer.alloc(count, char).toString(); + +/** + * Runs `bun ` in a directory holding `files`. Returns stderr and every + * source excerpt in it: a `N | text` line and the index of the `^` under it. + */ +async function printedExcerpts(args: string[], files: Record) { + using dir = tempDir("parse-col-excerpt", files); + await using proc = Bun.spawn({ + cmd: [bunExe(), ...args], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stderr] = await Promise.all([proc.stderr.text(), proc.stdout.text(), proc.exited]); + const lines = stderr.split(/\r?\n/); + const excerpts: { excerpt: string; caret: number }[] = []; + for (let i = 0; i + 1 < lines.length; i++) { + if (/^\d+ \| /.test(lines[i]) && /^ *\^$/.test(lines[i + 1])) { + excerpts.push({ excerpt: lines[i], caret: lines[i + 1].indexOf("^") }); + } + } + return { stderr, excerpts }; +} + +// The logger prints at most about 120 bytes of a line: 40 before the caret and +// 80 after it, or the last 120 when the line ends sooner. It indents the caret +// by what it printed. A location can hold far more than 120 bytes: the parsers +// keep the whole line for an error in its last 80 bytes, and CSS always does. +const longArray = "[" + fill(80_000, "1,") + "]"; + +describe.each([["build"], ["run"]])("bun %s prints a bounded excerpt of a very long line", subcommand => { + test.concurrent("for an error at its start", async () => { + const source = `var b = (; var a = ${longArray};`; + const { stderr, excerpts } = await printedExcerpts([subcommand, "long.js"], { "long.js": source }); + expect(excerpts).toEqual([{ excerpt: "1 | " + source.slice(0, 9 + 80), caret: 4 + 9 }]); + expect(stderr.length).toBeLessThan(1024); + }); + + test.concurrent("for an error in its middle", async () => { + const source = `var a = ${longArray}; var b = (; var c = ${longArray};`; + const at = source.indexOf("(;") + 1; + const { stderr, excerpts } = await printedExcerpts([subcommand, "long.js"], { "long.js": source }); + expect(excerpts.map(e => e.excerpt)).toEqual(["1 | " + source.slice(at - 40, at + 80)]); + // This location does not say how much of the line it dropped on the left, + // so only the length of the caret line is checked: it was `at` spaces long. + expect(excerpts[0].caret).toBeLessThanOrEqual(excerpts[0].excerpt.length); + expect(stderr.length).toBeLessThan(1024); + }); + + test.concurrent("for an error at its end", async () => { + const source = `var a = ${longArray}; var b = (;`; + const { stderr, excerpts } = await printedExcerpts([subcommand, "long.js"], { "long.js": source }); + expect(excerpts).toEqual([{ excerpt: "1 | " + source.slice(-120), caret: 4 + 119 }]); + expect(stderr.length).toBeLessThan(1024); + }); +}); + +test.concurrent("an error in the trailing whitespace of a very long line prints the end of its text", async () => { + // "Unexpected end of file" at the last of 200 spaces. Those 200 bytes are + // not the excerpt, and the caret goes right after the `(`. + const text = `var a = ${longArray}; var b = (`; + const { stderr, excerpts } = await printedExcerpts(["build", "long.js"], { "long.js": text + fill(200, " ") }); + expect(excerpts).toEqual([{ excerpt: "1 | " + text.slice(-120), caret: 4 + 120 }]); + expect(stderr.length).toBeLessThan(1024); +}); + +test.concurrent("an error in the trailing whitespace of a short line keeps its caret at the column", async () => { + const { excerpts } = await printedExcerpts(["build", "short.js"], { "short.js": "var b = (" + fill(20, " ") }); + expect(excerpts).toEqual([{ excerpt: "1 | var b = (", caret: 4 + 28 }]); +}); + +test.concurrent("an error and its note on one very long line both print a bounded excerpt", async () => { + const source = `const x = 1; var a = ${longArray}; const x = 2;`; + const { stderr, excerpts } = await printedExcerpts(["build", "long.js"], { "long.js": source }); + expect(excerpts).toEqual([ + { excerpt: "1 | " + source.slice(-120), caret: 4 + 120 - "x = 2;".length }, + { excerpt: "1 | " + source.slice(0, 6 + 80), caret: 4 + 6 }, + ]); + expect(stderr.length).toBeLessThan(1024); +}); + +test.concurrent.each([ + // The last 120 bytes start one byte into an `é` and three bytes into a `𐐀`. + ["2-byte", fill(600, "\u00E9") + "]", "1 | " + fill(120, "\u00E9") + "]", 4 + 60], + ["4-byte", fill(400, "\u{10400}") + "xy]", "1 | " + fill(120, "\u{10400}") + "xy]", 4 + 60 + 2], +])( + "bounded excerpt of a line of %s characters starts on a character and counts UTF-16 units", + async (_, source, excerpt, caret) => { + const { excerpts } = await printedExcerpts(["build", "long.js"], { "long.js": source }); + expect(excerpts).toEqual([{ excerpt, caret }]); + }, +); + +describe("bun build prints a bounded excerpt of a very long CSS line", () => { + // A stray `}` after a rule is an error at the `}`. + const rules = fill(60_000, "a{color:red}"); + + test.concurrent("for an error at its start", async () => { + const source = "a{color:red}}" + rules; + const { stderr, excerpts } = await printedExcerpts(["build", "long.css"], { "long.css": source }); + expect(excerpts).toEqual([{ excerpt: "1 | " + source.slice(0, 120), caret: 4 + 12 }]); + expect(stderr.length).toBeLessThan(1024); + }); + + test.concurrent("for an error in its middle", async () => { + const source = rules + "}" + rules; + const { stderr, excerpts } = await printedExcerpts(["build", "long.css"], { "long.css": source }); + expect(excerpts).toEqual([{ excerpt: "1 | " + source.slice(60_000 - 40, 60_000 + 80), caret: 4 + 40 }]); + expect(stderr.length).toBeLessThan(1024); + }); + + test.concurrent("for an error at its end", async () => { + // "Unexpected end of input": the caret is one column past the last `}`. + const source = rules + "}"; + const { stderr, excerpts } = await printedExcerpts(["build", "long.css"], { "long.css": source }); + expect(excerpts).toEqual([{ excerpt: "1 | " + source.slice(-120), caret: 4 + 120 }]); + expect(stderr.length).toBeLessThan(1024); + }); +}); + +test.concurrent("bounded excerpt of a CSS line starts and ends on a character", async () => { + // 40 bytes before the stray `}` is one byte into a `€`, and 80 bytes after + // it is two bytes into one. + const comment = "/*" + fill(60_000, "\u20AC") + "*/"; + const source = comment + "a{color:red}}" + comment + "a{color:red}"; + const { stderr, excerpts } = await printedExcerpts(["build", "long.css"], { "long.css": source }); + expect(excerpts).toEqual([ + { + excerpt: "1 | " + fill(27, "\u20AC") + "*/a{color:red}}/*" + fill(78, "\u20AC"), + caret: 4 + 9 + "*/a{color:red}".length, + }, + ]); + expect(stderr.length).toBeLessThan(1024); +}); + +test.concurrent("bun install prints a bounded excerpt of a one-line package.json that is cut short", async () => { + const source = `{"name":"x","version":"1.0.0","files":[` + fill(80_000, '"a",') + `"a"]`; + const { stderr, excerpts } = await printedExcerpts(["install"], { "package.json": source }); + expect(excerpts).toEqual([{ excerpt: "1 | " + source.slice(-120), caret: 4 + 119 }]); + expect(stderr.length).toBeLessThan(1024); }); From 86c3429d6cb2639e56d24d97cdb995f7fa1008e3 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:13:23 +0000 Subject: [PATCH 2/5] logger: print a redacted line whole The redaction marks a secret by the key in front of it. A cut 40 bytes before the caret can drop that key and print the secret. --- src/ast/lib.rs | 27 +++++++---- test/cli/install/redacted-config-logs.test.ts | 47 +++++++++++++++++++ 2 files changed, 65 insertions(+), 9 deletions(-) diff --git a/src/ast/lib.rs b/src/ast/lib.rs index fbe7b6d0b401..69faeba5afb1 100644 --- a/src/ast/lib.rs +++ b/src/ast/lib.rs @@ -983,6 +983,13 @@ impl Data { let (line_text, caret_column) = excerpt_around_column( line_text, usize::try_from(location.column - 1).expect("int cast"), + // The redaction has to see the whole line: the key that + // marks a secret can be far to the left of the caret. + if redact_sensitive_information { + usize::MAX + } else { + MAX_EXCERPT_LEN + }, ); let mut line_offset_for_second_line: usize = caret_column; @@ -1074,7 +1081,8 @@ fn write_n_bytes(to: &mut impl fmt::Write, b: u8, n: usize) -> fmt::Result { } /// `Data::write_format` prints at most this many bytes of a line, plus the -/// bytes that complete a UTF-8 sequence at either end. +/// bytes that complete a UTF-8 sequence at either end. It prints a redacted +/// line whole. const MAX_EXCERPT_LEN: usize = 120; /// The excerpt starts this many bytes before the caret, or earlier when the /// line ends before the excerpt is full. @@ -1094,19 +1102,20 @@ fn advance_utf16_columns(text: &[u8], columns: usize) -> (usize, usize) { (cursor.i as usize + cursor.width as usize, walked) } -/// The part of `line_text` that `Data::write_format` prints for a caret at the -/// 0-based `column`, and the caret's column in that part. Not every producer of -/// a `Location` bounds `line_text`, and `column` can lie past its end. -fn excerpt_around_column(line_text: &[u8], column: usize) -> (&[u8], usize) { +/// The part of `line_text`, at most about `max_len` bytes, that +/// `Data::write_format` prints for a caret at the 0-based `column`, and the +/// caret's column in that part. Not every producer of a `Location` bounds +/// `line_text`, and `column` can lie past its end. +fn excerpt_around_column(line_text: &[u8], column: usize, max_len: usize) -> (&[u8], usize) { + debug_assert!(max_len >= EXCERPT_LEN_BEFORE_CARET); let (caret, caret_column) = advance_utf16_columns(line_text, column); let visible = bun_core::trim_right(line_text, b" \r\n\t"); - if line_text.len() <= MAX_EXCERPT_LEN { + if line_text.len() <= max_len { return (visible, caret_column); } let caret = caret.min(visible.len()); - let mut hi = - (caret.saturating_sub(EXCERPT_LEN_BEFORE_CARET) + MAX_EXCERPT_LEN).min(visible.len()); - let mut lo = hi.saturating_sub(MAX_EXCERPT_LEN); + let mut hi = (caret.saturating_sub(EXCERPT_LEN_BEFORE_CARET) + max_len).min(visible.len()); + let mut lo = hi.saturating_sub(max_len); // A UTF-8 sequence has at most 3 continuation bytes. for _ in 0..3 { if !bun_core::strings::is_on_char_boundary(visible, lo) { diff --git a/test/cli/install/redacted-config-logs.test.ts b/test/cli/install/redacted-config-logs.test.ts index 6c49d43123b4..8bbea4daa407 100644 --- a/test/cli/install/redacted-config-logs.test.ts +++ b/test/cli/install/redacted-config-logs.test.ts @@ -141,6 +141,53 @@ test("bunfig password value is masked in config error output", async () => { expect(coloredExit).toBe(1); }); +test("bunfig token is masked when its key is further from the error than one printed excerpt", async () => { + // The logger prints about 120 bytes around the caret of a long line. The + // `token` key is 154 bytes to the left of the caret here, and the redaction + // has to see that key to know that the string is a secret. + const secret = Buffer.alloc(143, "secretvalue").toString(); + const masked = Buffer.alloc(143, "*").toString(); + using dir = tempDir("redacted-bunfig-long-line", { + "bunfig.toml": `[install]\ntoken = "${secret}" ]\n`, + "package.json": "{}", + }); + + await using plain = Bun.spawn({ + cmd: [bunExe(), "install"], + cwd: String(dir), + env: { ...bunEnv, NO_COLOR: "1" }, + stdout: "pipe", + stderr: "pipe", + }); + + const [plainOut, plainErr, plainExit] = await Promise.all([plain.stdout.text(), plain.stderr.text(), plain.exited]); + + expect(plainOut).not.toContain("secretvalue"); + expect(plainErr).not.toContain("secretvalue"); + expect(plainErr).toContain(`2 | token = "${masked}" ]`); + + await using colored = Bun.spawn({ + cmd: [bunExe(), "install"], + cwd: String(dir), + env: { ...bunEnv, NO_COLOR: undefined, FORCE_COLOR: "1" }, + stdout: "pipe", + stderr: "pipe", + }); + + const [coloredOut, coloredErr, coloredExit] = await Promise.all([ + colored.stdout.text(), + colored.stderr.text(), + colored.exited, + ]); + + expect(coloredOut).not.toContain("secretvalue"); + expect(coloredErr).not.toContain("secretvalue"); + expect(coloredErr).toContain(`"${masked}"`); + + expect(plainExit).toBe(1); + expect(coloredExit).toBe(1); +}); + describe.concurrent("redact", async () => { const tests = [ { From e8f7683a8cc007c46fc3c94c95f23635e875c98e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:44:17 +0000 Subject: [PATCH 3/5] test: assert the exit code in the excerpt tests --- .../bun/transpiler/parse-error-column.test.ts | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/test/js/bun/transpiler/parse-error-column.test.ts b/test/js/bun/transpiler/parse-error-column.test.ts index a2d9167d291e..837d88a5682f 100644 --- a/test/js/bun/transpiler/parse-error-column.test.ts +++ b/test/js/bun/transpiler/parse-error-column.test.ts @@ -164,8 +164,9 @@ test.concurrent("CLI caret stays under the token for an error at the end of a lo const fill = (count: number, char: string) => Buffer.alloc(count, char).toString(); /** - * Runs `bun ` in a directory holding `files`. Returns stderr and every - * source excerpt in it: a `N | text` line and the index of the `^` under it. + * Runs `bun ` in a directory holding `files`. Returns the exit code, + * stderr and every source excerpt in it: a `N | text` line and the index of + * the `^` under it. */ async function printedExcerpts(args: string[], files: Record) { using dir = tempDir("parse-col-excerpt", files); @@ -176,7 +177,7 @@ async function printedExcerpts(args: string[], files: Record) { stdout: "pipe", stderr: "pipe", }); - const [stderr] = await Promise.all([proc.stderr.text(), proc.stdout.text(), proc.exited]); + const [stderr, , exitCode] = await Promise.all([proc.stderr.text(), proc.stdout.text(), proc.exited]); const lines = stderr.split(/\r?\n/); const excerpts: { excerpt: string; caret: number }[] = []; for (let i = 0; i + 1 < lines.length; i++) { @@ -184,7 +185,7 @@ async function printedExcerpts(args: string[], files: Record) { excerpts.push({ excerpt: lines[i], caret: lines[i + 1].indexOf("^") }); } } - return { stderr, excerpts }; + return { stderr, excerpts, exitCode }; } // The logger prints at most about 120 bytes of a line: 40 before the caret and @@ -196,27 +197,30 @@ const longArray = "[" + fill(80_000, "1,") + "]"; describe.each([["build"], ["run"]])("bun %s prints a bounded excerpt of a very long line", subcommand => { test.concurrent("for an error at its start", async () => { const source = `var b = (; var a = ${longArray};`; - const { stderr, excerpts } = await printedExcerpts([subcommand, "long.js"], { "long.js": source }); + const { stderr, excerpts, exitCode } = await printedExcerpts([subcommand, "long.js"], { "long.js": source }); expect(excerpts).toEqual([{ excerpt: "1 | " + source.slice(0, 9 + 80), caret: 4 + 9 }]); expect(stderr.length).toBeLessThan(1024); + expect(exitCode).toBe(1); }); test.concurrent("for an error in its middle", async () => { const source = `var a = ${longArray}; var b = (; var c = ${longArray};`; const at = source.indexOf("(;") + 1; - const { stderr, excerpts } = await printedExcerpts([subcommand, "long.js"], { "long.js": source }); + const { stderr, excerpts, exitCode } = await printedExcerpts([subcommand, "long.js"], { "long.js": source }); expect(excerpts.map(e => e.excerpt)).toEqual(["1 | " + source.slice(at - 40, at + 80)]); // This location does not say how much of the line it dropped on the left, // so only the length of the caret line is checked: it was `at` spaces long. expect(excerpts[0].caret).toBeLessThanOrEqual(excerpts[0].excerpt.length); expect(stderr.length).toBeLessThan(1024); + expect(exitCode).toBe(1); }); test.concurrent("for an error at its end", async () => { const source = `var a = ${longArray}; var b = (;`; - const { stderr, excerpts } = await printedExcerpts([subcommand, "long.js"], { "long.js": source }); + const { stderr, excerpts, exitCode } = await printedExcerpts([subcommand, "long.js"], { "long.js": source }); expect(excerpts).toEqual([{ excerpt: "1 | " + source.slice(-120), caret: 4 + 119 }]); expect(stderr.length).toBeLessThan(1024); + expect(exitCode).toBe(1); }); }); @@ -224,24 +228,31 @@ test.concurrent("an error in the trailing whitespace of a very long line prints // "Unexpected end of file" at the last of 200 spaces. Those 200 bytes are // not the excerpt, and the caret goes right after the `(`. const text = `var a = ${longArray}; var b = (`; - const { stderr, excerpts } = await printedExcerpts(["build", "long.js"], { "long.js": text + fill(200, " ") }); + const { stderr, excerpts, exitCode } = await printedExcerpts(["build", "long.js"], { + "long.js": text + fill(200, " "), + }); expect(excerpts).toEqual([{ excerpt: "1 | " + text.slice(-120), caret: 4 + 120 }]); expect(stderr.length).toBeLessThan(1024); + expect(exitCode).toBe(1); }); test.concurrent("an error in the trailing whitespace of a short line keeps its caret at the column", async () => { - const { excerpts } = await printedExcerpts(["build", "short.js"], { "short.js": "var b = (" + fill(20, " ") }); + const { excerpts, exitCode } = await printedExcerpts(["build", "short.js"], { + "short.js": "var b = (" + fill(20, " "), + }); expect(excerpts).toEqual([{ excerpt: "1 | var b = (", caret: 4 + 28 }]); + expect(exitCode).toBe(1); }); test.concurrent("an error and its note on one very long line both print a bounded excerpt", async () => { const source = `const x = 1; var a = ${longArray}; const x = 2;`; - const { stderr, excerpts } = await printedExcerpts(["build", "long.js"], { "long.js": source }); + const { stderr, excerpts, exitCode } = await printedExcerpts(["build", "long.js"], { "long.js": source }); expect(excerpts).toEqual([ { excerpt: "1 | " + source.slice(-120), caret: 4 + 120 - "x = 2;".length }, { excerpt: "1 | " + source.slice(0, 6 + 80), caret: 4 + 6 }, ]); expect(stderr.length).toBeLessThan(1024); + expect(exitCode).toBe(1); }); test.concurrent.each([ @@ -251,8 +262,9 @@ test.concurrent.each([ ])( "bounded excerpt of a line of %s characters starts on a character and counts UTF-16 units", async (_, source, excerpt, caret) => { - const { excerpts } = await printedExcerpts(["build", "long.js"], { "long.js": source }); + const { excerpts, exitCode } = await printedExcerpts(["build", "long.js"], { "long.js": source }); expect(excerpts).toEqual([{ excerpt, caret }]); + expect(exitCode).toBe(1); }, ); @@ -262,24 +274,27 @@ describe("bun build prints a bounded excerpt of a very long CSS line", () => { test.concurrent("for an error at its start", async () => { const source = "a{color:red}}" + rules; - const { stderr, excerpts } = await printedExcerpts(["build", "long.css"], { "long.css": source }); + const { stderr, excerpts, exitCode } = await printedExcerpts(["build", "long.css"], { "long.css": source }); expect(excerpts).toEqual([{ excerpt: "1 | " + source.slice(0, 120), caret: 4 + 12 }]); expect(stderr.length).toBeLessThan(1024); + expect(exitCode).toBe(1); }); test.concurrent("for an error in its middle", async () => { const source = rules + "}" + rules; - const { stderr, excerpts } = await printedExcerpts(["build", "long.css"], { "long.css": source }); + const { stderr, excerpts, exitCode } = await printedExcerpts(["build", "long.css"], { "long.css": source }); expect(excerpts).toEqual([{ excerpt: "1 | " + source.slice(60_000 - 40, 60_000 + 80), caret: 4 + 40 }]); expect(stderr.length).toBeLessThan(1024); + expect(exitCode).toBe(1); }); test.concurrent("for an error at its end", async () => { // "Unexpected end of input": the caret is one column past the last `}`. const source = rules + "}"; - const { stderr, excerpts } = await printedExcerpts(["build", "long.css"], { "long.css": source }); + const { stderr, excerpts, exitCode } = await printedExcerpts(["build", "long.css"], { "long.css": source }); expect(excerpts).toEqual([{ excerpt: "1 | " + source.slice(-120), caret: 4 + 120 }]); expect(stderr.length).toBeLessThan(1024); + expect(exitCode).toBe(1); }); }); @@ -288,7 +303,7 @@ test.concurrent("bounded excerpt of a CSS line starts and ends on a character", // it is two bytes into one. const comment = "/*" + fill(60_000, "\u20AC") + "*/"; const source = comment + "a{color:red}}" + comment + "a{color:red}"; - const { stderr, excerpts } = await printedExcerpts(["build", "long.css"], { "long.css": source }); + const { stderr, excerpts, exitCode } = await printedExcerpts(["build", "long.css"], { "long.css": source }); expect(excerpts).toEqual([ { excerpt: "1 | " + fill(27, "\u20AC") + "*/a{color:red}}/*" + fill(78, "\u20AC"), @@ -296,11 +311,13 @@ test.concurrent("bounded excerpt of a CSS line starts and ends on a character", }, ]); expect(stderr.length).toBeLessThan(1024); + expect(exitCode).toBe(1); }); test.concurrent("bun install prints a bounded excerpt of a one-line package.json that is cut short", async () => { const source = `{"name":"x","version":"1.0.0","files":[` + fill(80_000, '"a",') + `"a"]`; - const { stderr, excerpts } = await printedExcerpts(["install"], { "package.json": source }); + const { stderr, excerpts, exitCode } = await printedExcerpts(["install"], { "package.json": source }); expect(excerpts).toEqual([{ excerpt: "1 | " + source.slice(-120), caret: 4 + 119 }]); expect(stderr.length).toBeLessThan(1024); + expect(exitCode).toBe(1); }); From 239fad00dc64d0afa551fc15a1bd00d614931a56 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:46:46 +0000 Subject: [PATCH 4/5] logger: shorten comments --- src/ast/lib.rs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/src/ast/lib.rs b/src/ast/lib.rs index 69faeba5afb1..a5f49ba394c6 100644 --- a/src/ast/lib.rs +++ b/src/ast/lib.rs @@ -983,8 +983,7 @@ impl Data { let (line_text, caret_column) = excerpt_around_column( line_text, usize::try_from(location.column - 1).expect("int cast"), - // The redaction has to see the whole line: the key that - // marks a secret can be far to the left of the caret. + // A cut can drop the key that marks a secret, so a redacted line prints whole. if redact_sensitive_information { usize::MAX } else { @@ -1080,17 +1079,12 @@ fn write_n_bytes(to: &mut impl fmt::Write, b: u8, n: usize) -> fmt::Result { Ok(()) } -/// `Data::write_format` prints at most this many bytes of a line, plus the -/// bytes that complete a UTF-8 sequence at either end. It prints a redacted -/// line whole. +/// `Data::write_format` cuts a line longer than this many bytes, unless it redacts the line. const MAX_EXCERPT_LEN: usize = 120; -/// The excerpt starts this many bytes before the caret, or earlier when the -/// line ends before the excerpt is full. +/// The excerpt starts this many bytes before the caret, or earlier when the line ends first. const EXCERPT_LEN_BEFORE_CARET: usize = 40; -/// Walks `text` until `columns` UTF-16 code units, the unit of -/// `Location::column`, have gone by or `text` ends. Returns the bytes and the -/// code units walked. +/// Walks `text` for up to `columns` UTF-16 code units. Returns the bytes and the code units walked. fn advance_utf16_columns(text: &[u8], columns: usize) -> (usize, usize) { use bun_core::strings::{CodepointIterator, Cursor}; let iter = CodepointIterator::init(text); @@ -1102,10 +1096,7 @@ fn advance_utf16_columns(text: &[u8], columns: usize) -> (usize, usize) { (cursor.i as usize + cursor.width as usize, walked) } -/// The part of `line_text`, at most about `max_len` bytes, that -/// `Data::write_format` prints for a caret at the 0-based `column`, and the -/// caret's column in that part. Not every producer of a `Location` bounds -/// `line_text`, and `column` can lie past its end. +/// The part of `line_text` to print for a caret at the 0-based `column`, and the caret's column in it. fn excerpt_around_column(line_text: &[u8], column: usize, max_len: usize) -> (&[u8], usize) { debug_assert!(max_len >= EXCERPT_LEN_BEFORE_CARET); let (caret, caret_column) = advance_utf16_columns(line_text, column); From 4add19d2da0ded6a4665eaf5f7051cf03dffae26 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 18 Sep 2026 17:36:39 +0000 Subject: [PATCH 5/5] ci: retrigger