Skip to content
Open
44 changes: 33 additions & 11 deletions src/jsc/ConsoleObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4940,8 +4940,13 @@ pub mod formatter {
}

let event_type_value: JSValue = 'brk: {
let Some(value_) = value.get(self.global_this, "type")? else {
break 'brk JSValue::UNDEFINED;
let value_ = match value.get(self.global_this, "type") {
Ok(Some(v)) => v,
Ok(None) => break 'brk JSValue::UNDEFINED,
Err(_) => {
self.global_this.clear_exception();
break 'brk JSValue::UNDEFINED;
}
};
Comment thread
robobun marked this conversation as resolved.
if value_.is_string() {
break 'brk value_;
Expand Down Expand Up @@ -5015,9 +5020,15 @@ pub mod formatter {
);
}

if let Some(message_value) =
value.fast_get(self.global_this, jsc::BuiltinName::Message)?
{
let message_value =
match value.fast_get(self.global_this, jsc::BuiltinName::Message) {
Ok(v) => v,
Err(_) => {
self.global_this.clear_exception();
None
}
};
if let Some(message_value) = message_value {
if message_value.is_string() {
if !self.single_line {
self.write_indent(writer_).expect("unreachable");
Expand Down Expand Up @@ -5054,9 +5065,14 @@ pub mod formatter {
pf!("<d>"),
pf!("<r>")
);
let data: JSValue = value
.fast_get(self.global_this, jsc::BuiltinName::Data)?
.unwrap_or(JSValue::UNDEFINED);
let data: JSValue =
match value.fast_get(self.global_this, jsc::BuiltinName::Data) {
Ok(v) => v.unwrap_or(JSValue::UNDEFINED),
Err(_) => {
self.global_this.clear_exception();
JSValue::UNDEFINED
}
};
let tag = Tag::get_advanced(data, self.global_this, self.tag_opts())?;
self.format::<C>(tag, writer_, data, self.global_this)?;
if self.failed {
Expand All @@ -5068,9 +5084,15 @@ pub mod formatter {
}
}
EventType::ErrorEvent => {
if let Some(error_value) =
value.fast_get(self.global_this, jsc::BuiltinName::Error)?
{
let error_value =
match value.fast_get(self.global_this, jsc::BuiltinName::Error) {
Ok(v) => v,
Err(_) => {
self.global_this.clear_exception();
None
}
};
if let Some(error_value) = error_value {
if !self.single_line {
self.write_indent(writer_).expect("unreachable");
}
Expand Down
12 changes: 8 additions & 4 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4839,11 +4839,15 @@ impl VirtualMachine {
allow_ansi_color,
allow_side_effects,
};
// `getErrorsProperty` is
// `getDirect` (own data prop, nothrow); `for_each` may throw, in
// which case the error is swallowed.
// `getDirect` may hand back empty or a GetterSetter; `is_object()` rejects both.
let errors = value.get_errors_property(global_ref);
let _ = errors.for_each(global_ref, (&raw mut ctx).cast(), agg_iter);
if errors.is_object()
&& errors
.for_each(global_ref, (&raw mut ctx).cast(), agg_iter)
.is_err()
{
global_ref.clear_exception();
}
return;
}

Expand Down
44 changes: 34 additions & 10 deletions src/runtime/test_runner/pretty_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1898,9 +1898,13 @@ impl<'a> Formatter<'a> {
}
Tag::Event => {
let event_type_value: JSValue = 'brk: {
let value_: JSValue = match value.get(self.global_this, "type")? {
Some(v) => v,
None => break 'brk JSValue::UNDEFINED,
let value_: JSValue = match value.get(self.global_this, "type") {
Ok(Some(v)) => v,
Ok(None) => break 'brk JSValue::UNDEFINED,
Err(_) => {
self.global_this.clear_exception();
break 'brk JSValue::UNDEFINED;
}
};
if value_.is_string() {
break 'brk value_;
Expand Down Expand Up @@ -1947,9 +1951,16 @@ impl<'a> Formatter<'a> {
pretty_fmt_const::<ENABLE_ANSI_COLORS>("<r>"),
));

if let Some(message_value) =
value.fast_get(self.global_this, jsc::BuiltinName::Message)?
let message_value = match value
.fast_get(self.global_this, jsc::BuiltinName::Message)
{
Ok(v) => v,
Err(_) => {
self.global_this.clear_exception();
None
}
};
if let Some(message_value) = message_value {
if message_value.is_string() {
self.write_indent(writer.ctx).expect("unreachable");
writer.print(format_args!(
Expand All @@ -1976,9 +1987,15 @@ impl<'a> Formatter<'a> {
pretty_fmt_const::<ENABLE_ANSI_COLORS>("<d>"),
pretty_fmt_const::<ENABLE_ANSI_COLORS>("<r>"),
));
let data: JSValue = value
.fast_get(self.global_this, jsc::BuiltinName::Data)?
.unwrap_or(JSValue::UNDEFINED);
let data: JSValue = match value
.fast_get(self.global_this, jsc::BuiltinName::Data)
{
Ok(v) => v.unwrap_or(JSValue::UNDEFINED),
Err(_) => {
self.global_this.clear_exception();
JSValue::UNDEFINED
}
};
let tag = Tag::get(data, self.global_this)?;

self.format::<W, ENABLE_ANSI_COLORS>(
Expand All @@ -1987,9 +2004,16 @@ impl<'a> Formatter<'a> {
writer.write_all(b", \n");
}
EventType::ErrorEvent => {
if let Some(data) =
value.fast_get(self.global_this, jsc::BuiltinName::Error)?
let error_value = match value
.fast_get(self.global_this, jsc::BuiltinName::Error)
{
Ok(v) => v,
Err(_) => {
self.global_this.clear_exception();
None
}
};
if let Some(data) = error_value {
self.write_indent(writer.ctx).expect("unreachable");
writer.print(format_args!(
"{}error{}:{} ",
Expand Down
103 changes: 102 additions & 1 deletion test/js/bun/util/inspect.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "bun:test";
import { bunEnv, bunExe, normalizeBunSnapshot, tmpdirSync } from "harness";
import { bunEnv, bunExe, normalizeBunSnapshot, tempDir, tmpdirSync } from "harness";
import { join } from "path";
import util from "util";
it("prototype", () => {
Expand Down Expand Up @@ -205,6 +205,107 @@ it("MessageEvent with deleted data", () => {
);
});

it("Event subclass with a throwing getter does not make Bun.inspect throw", () => {
class ThrowType extends Event {
get type() {
throw new Error("type-getter-boom");
}
}
const typeOut = Bun.inspect(new ThrowType("t"));
expect(typeOut).toContain("type: [Getter]");
expect(typeOut).not.toContain("type-getter-boom");
expect(() => Bun.inspect({ payload: [new ThrowType("t")] })).not.toThrow();

class ThrowData extends MessageEvent {
get data() {
throw new Error("data-getter-boom");
}
}
expect(Bun.inspect(new ThrowData("message", { data: "p" }))).toBe(
`MessageEvent {\n type: "message",\n data: undefined,\n}`,
);

class ThrowError extends ErrorEvent {
get error() {
throw new Error("error-getter-boom");
}
get message() {
throw new Error("message-getter-boom");
}
}
expect(Bun.inspect(new ThrowError("error", { message: "m", error: new Error("i") }))).toBe(
`ErrorEvent {\n type: "error",\n}`,
);

// Own-instance accessors (not subclass) on the Event branch reads.
const me = new MessageEvent("message", { data: "p" });
Object.defineProperty(me, "data", {
get() {
throw new Error("own-data-boom");
},
configurable: true,
});
expect(Bun.inspect(me)).toBe(`MessageEvent {\n type: "message",\n data: undefined,\n}`);
expect(Bun.inspect({ nested: me })).toContain("data: undefined");
});

it("AggregateError with a hostile 'errors' property does not make Bun.inspect throw", () => {
// Accessor own prop: getDirect returns the GetterSetter cell, for_each throws.
const a = new AggregateError([new Error("x")], "agg");
Object.defineProperty(a, "errors", {
get() {
throw new Error("errors-getter-boom");
},
configurable: true,
});
expect(() => Bun.inspect(a)).not.toThrow();
expect(() => Bun.inspect({ nested: a })).not.toThrow();

// Non-iterable data prop: for_each throws TypeError.
const b = new AggregateError([new Error("x")], "agg");
b.errors = { not: "iterable" };
expect(() => Bun.inspect(b)).not.toThrow();

// Deleted own prop: getDirect returns empty; used to segfault in for_each.
const c = new AggregateError([new Error("x")], "agg");
delete c.errors;
expect(() => Bun.inspect(c)).not.toThrow();
});

it("Event subclass with a throwing getter does not make toMatchSnapshot fail", async () => {
using dir = tempDir("inspect-event-snapshot", {
"snap.test.js": `
import { test, expect } from "bun:test";
test("type", () => {
class E extends Event { get type() { throw new Error("type-getter-boom"); } }
expect(new E("t")).toMatchSnapshot();
});
test("data", () => {
class M extends Event { get data() { throw new Error("data-getter-boom"); } }
expect(new M("message")).toMatchSnapshot();
});
test("error", () => {
class R extends Event {
get error() { throw new Error("error-getter-boom"); }
get message() { throw new Error("message-getter-boom"); }
}
expect(new R("error")).toMatchSnapshot();
});
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test", "--update-snapshots", "snap.test.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const all = stdout + stderr;
expect(all).not.toContain("Failed to pretty format");
expect(all).toContain("3 pass");
expect(exitCode).toBe(0);
});

// https://github.com/oven-sh/bun/issues/561
it("TypedArray prints", () => {
for (let TypedArray of [
Expand Down
Loading