fix: panic when overriding Set/Map size property with non-numeric value (#23787)

## Summary

Fixes a panic that occurred when `console.log()` tried to format a Set
or Map instance with a non-numeric `size` property.

## Issue

When a Set or Map subclass overrides the `size` property with a
non-numeric value (like a constructor function, string, or other
object), calling `console.log()` on the instance would trigger a panic:

```javascript
class C1 extends Set {
    constructor() {
        super();
        Object.defineProperty(this, "size", {
            writable: true,
            enumerable: true,
            value: Set
        });
        console.log(this); // panic!
    }
}
new C1();
```

## Root Cause

In `src/bun.js/ConsoleObject.zig`, the Map and Set formatting code
called `toInt32()` directly on the `size` property value. This function
asserts that the value is not a Cell (objects/functions), causing a
panic when `size` was overridden with non-numeric values.

## Solution

Changed both Map and Set formatting to use `coerce(i32, globalThis)`
instead of `toInt32()`. This properly handles non-numeric values using
JavaScript's standard type coercion rules and propagates any coercion
errors appropriately.

## Test Plan

Added regression tests to `test/js/bun/util/inspect.test.js` that verify
Set and Map instances with overridden non-numeric `size` properties can
be inspected without panicking.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
robobun
2025-10-17 14:03:26 -07:00
committed by GitHub
parent 28f0e5b3b5
commit 1abfc0ea24
2 changed files with 19 additions and 2 deletions

View File

@@ -2717,7 +2717,7 @@ pub const Formatter = struct {
},
.Map => {
const length_value = try value.get(this.globalThis, "size") orelse jsc.JSValue.jsNumberFromInt32(0);
const length = length_value.toInt32();
const length = try length_value.coerce(i32, this.globalThis);
const prev_quote_strings = this.quote_strings;
this.quote_strings = true;
@@ -2824,7 +2824,7 @@ pub const Formatter = struct {
},
.Set => {
const length_value = try value.get(this.globalThis, "size") orelse jsc.JSValue.jsNumberFromInt32(0);
const length = length_value.toInt32();
const length = try length_value.coerce(i32, this.globalThis);
const prev_quote_strings = this.quote_strings;
this.quote_strings = true;

View File

@@ -347,6 +347,23 @@ it("inspect", () => {
expect(Bun.inspect(new Map())).toBe("Map {}");
expect(Bun.inspect(new Map([["foo", "bar"]]))).toBe('Map(1) {\n "foo": "bar",\n}');
expect(Bun.inspect(new Set(["bar"]))).toBe('Set(1) {\n "bar",\n}');
// Regression test: Set/Map with overridden size property should not panic
const setWithOverriddenSize = new Set();
Object.defineProperty(setWithOverriddenSize, "size", {
writable: true,
enumerable: true,
value: Set,
});
expect(Bun.inspect(setWithOverriddenSize)).toBe("Set {}");
const mapWithOverriddenSize = new Map();
Object.defineProperty(mapWithOverriddenSize, "size", {
writable: true,
enumerable: true,
value: "not a number",
});
expect(Bun.inspect(mapWithOverriddenSize)).toBe("Map {}");
expect(Bun.inspect(<div>foo</div>)).toBe("<div>foo</div>");
expect(Bun.inspect(<div hello>foo</div>)).toBe("<div hello=true>foo</div>");
expect(Bun.inspect(<div hello={1}>foo</div>)).toBe("<div hello=1>foo</div>");