From da9ef23d54402e4d904f8f32eec899a23838ce16 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 31 Oct 2026 18:37:29 +0000 Subject: [PATCH 1/4] console: print String.prototype, Number.prototype and Boolean.prototype as {} In JSC these three prototypes are boxed values of "", 0 and false. Tag::get_advanced picks the printer from the cell type, so console.log and Bun.inspect printed them as an empty line, [Number: 0] and [Boolean: false]. Tag::get_advanced now sends them to the object printer. One predicate finds them: String.prototype by its cell type, which no other cell has, and the other two by their class. The object printer gives them no class name, and the property walk lists only what a program put on them. Fixes #8627 --- src/jsc/ConsoleObject.rs | 18 +++- src/jsc/JSType.rs | 1 + src/jsc/bindings/UtilInspect.cpp | 6 ++ src/jsc/bindings/UtilInspect.h | 25 +++++ src/jsc/bindings/bindings.cpp | 10 +- test/bundler/transpiler/macro-test.test.ts | 6 ++ test/bundler/transpiler/macro.ts | 4 + .../snapshot-tests/snapshots/snapshot.test.ts | 45 +++++++++ test/js/bun/util/inspect.test.js | 96 +++++++++++++++++++ test/js/web/console/console-log.expected.txt | 3 + test/js/web/console/console-log.js | 3 + 11 files changed, 213 insertions(+), 4 deletions(-) diff --git a/src/jsc/ConsoleObject.rs b/src/jsc/ConsoleObject.rs index 8a0d6bfe78e3..40f3eccdcd83 100644 --- a/src/jsc/ConsoleObject.rs +++ b/src/jsc/ConsoleObject.rs @@ -2203,13 +2203,22 @@ pub mod formatter { use jsc::JSType as T; let tag = match js_type { T::ErrorInstance => TagPayload::Error, + // JSC makes these three prototypes boxed values (#8627). `cell` selects string colouring. + T::DerivedStringObject | T::NumberObject | T::BooleanObject + if crate::cpp::Bun__isStringNumberOrBooleanPrototype(value) => + { + return Ok(TagResult { + tag: TagPayload::Object, + cell: T::Object, + }); + } T::NumberObject => TagPayload::Double, T::DerivedArray | T::Array | T::DirectArguments | T::ScopedArguments | T::ClonedArguments => TagPayload::Array, - T::DerivedStringObject | T::String | T::StringObject => TagPayload::String, + T::String | T::StringObject => TagPayload::String, T::RegExpObject => TagPayload::String, T::Symbol => TagPayload::Symbol, T::BooleanObject => TagPayload::Boolean, @@ -3166,6 +3175,10 @@ pub mod formatter { ) -> JsResult> { let name_str = value.get_class_name(global_this)?; if !name_str.eq_ascii(b"Object") { + // Node prints `String.prototype`, `Number.prototype` and `Boolean.prototype` as `{}`. + if crate::cpp::Bun__isStringNumberOrBooleanPrototype(value) { + return Ok(None); + } return Ok(Some(name_str)); } else if value.get_prototype(global_this)?.eql_value(JSValue::NULL) { return Ok(Some(bun_core::String::static_("[Object: null prototype]"))); @@ -5366,7 +5379,8 @@ pub mod formatter { i: 0, }; - if ordered_properties { + // Only the other walk leaves out the built-in methods of `String.prototype` and its two siblings. + if ordered_properties && !crate::cpp::Bun__isStringNumberOrBooleanPrototype(value) { value.for_each_property_ordered( global_this, (&raw mut iter).cast::(), diff --git a/src/jsc/JSType.rs b/src/jsc/JSType.rs index 77e90f5cbf21..9b745359edd4 100644 --- a/src/jsc/JSType.rs +++ b/src/jsc/JSType.rs @@ -547,6 +547,7 @@ impl JSType { /// ``` pub const StringObject: JSType = JSType(100); + /// `String.prototype` of any realm, and nothing else. An instance of a subclass is a `StringObject`. pub const DerivedStringObject: JSType = JSType(101); pub const Event: JSType = JSType(0b11101111); diff --git a/src/jsc/bindings/UtilInspect.cpp b/src/jsc/bindings/UtilInspect.cpp index 85d25bf22bfd..0afa91e25807 100644 --- a/src/jsc/bindings/UtilInspect.cpp +++ b/src/jsc/bindings/UtilInspect.cpp @@ -11,6 +11,7 @@ #include "JavaScriptCore/PropertyNameArray.h" #include "JavaScriptCore/JSArray.h" #include "JavaScriptCore/IdentifierInlines.h" +#include "UtilInspect.h" namespace Bun { @@ -73,6 +74,11 @@ extern "C" JSC::EncodedJSValue JSC__JSValue__callCustomInspectFunction( RELEASE_AND_RETURN(scope, JSValue::encode(inspectRet)); } +extern "C" [[ZIG_EXPORT(nothrow)]] bool Bun__isStringNumberOrBooleanPrototype(JSC::EncodedJSValue encodedValue) +{ + return isStringNumberOrBooleanPrototype(JSValue::decode(encodedValue)); +} + // Port of V8's `internalBinding('util').getOwnNonIndexProperties(object, filter)` used by // util.inspect and the REPL completer: own keys minus array indices, without materializing // a name (or descriptor) per element the way Object.getOwnPropertyNames() on an array would. diff --git a/src/jsc/bindings/UtilInspect.h b/src/jsc/bindings/UtilInspect.h index ea2d39848a40..1234dffe3b1b 100644 --- a/src/jsc/bindings/UtilInspect.h +++ b/src/jsc/bindings/UtilInspect.h @@ -1,9 +1,34 @@ #pragma once #include "root.h" +#include "JavaScriptCore/BooleanPrototype.h" +#include "JavaScriptCore/NumberPrototype.h" namespace Bun { +// String.prototype, Number.prototype or Boolean.prototype of any realm. JSC makes them boxed values of "", 0 and false. +ALWAYS_INLINE bool isStringNumberOrBooleanPrototype(JSC::JSCell* cell) +{ + // Each of the three has a static property table, and few printed objects do: most values pay for this test only. + if (!JSC::TypeInfo::hasStaticPropertyTable(cell->inlineTypeFlags())) + return false; + switch (cell->type()) { + case JSC::DerivedStringObjectType: + return true; + case JSC::NumberObjectType: + return cell->classInfo() == JSC::NumberPrototype::info(); + case JSC::BooleanObjectType: + return cell->classInfo() == JSC::BooleanPrototype::info(); + default: + return false; + } +} + +ALWAYS_INLINE bool isStringNumberOrBooleanPrototype(JSC::JSValue value) +{ + return value.isCell() && isStringNumberOrBooleanPrototype(value.asCell()); +} + JSC::Structure* createUtilInspectOptionsStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject); // internalBinding('util').getOwnNonIndexProperties(object, filter) diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index cbc4d960e2f4..ed668269ecbd 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -136,6 +136,7 @@ #include "JavaScriptCore/TimeZoneICUBridge.h" #include "JavaScriptCore/FunctionPrototype.h" +#include "UtilInspect.h" #include "JSFetchHeaders.h" #include "FetchHeaders.h" #include "BunCompiledObjectEquality.h" @@ -5614,6 +5615,11 @@ static void JSC__JSValue__forEachPropertyImpl(JSC::EncodedJSValue JSValue0, JSC: bool hasNoOwnProperty = structure->outOfLineSize() == 0 && structure->inlineSize() == 0; #endif + // These three prototypes show what a program put on them. The quick walk lists each method of a reified one. + const bool listsOwnEnumerableOnly = Bun::isStringNumberOrBooleanPrototype(object); + if (listsOwnEnumerableOnly) + fast = false; + if (fast) { if (hasNoOwnProperty) { fast = false; @@ -5754,7 +5760,7 @@ static void JSC__JSValue__forEachPropertyImpl(JSC::EncodedJSValue JSValue0, JSC: if constexpr (nonIndexedOnly) { iterating->getOwnNonIndexPropertyNames(globalObject, properties, DontEnumPropertiesMode::Include); } else { - iterating->methodTable()->getOwnPropertyNames(iterating, globalObject, properties, isOtherGlobal ? DontEnumPropertiesMode::Exclude : DontEnumPropertiesMode::Include); + iterating->methodTable()->getOwnPropertyNames(iterating, globalObject, properties, (isOtherGlobal || listsOwnEnumerableOnly) ? DontEnumPropertiesMode::Exclude : DontEnumPropertiesMode::Include); } RETURN_IF_EXCEPTION(scope, void()); @@ -5848,7 +5854,7 @@ static void JSC__JSValue__forEachPropertyImpl(JSC::EncodedJSValue JSValue0, JSC: properties.data()->propertyNameVector().shrink(0); if (iterating->isCallable()) break; - if (iterating == globalObject || isOtherGlobal) + if (iterating == globalObject || isOtherGlobal || listsOwnEnumerableOnly) break; JSValue prototype = iterating->getPrototype(globalObject); RETURN_IF_EXCEPTION(scope, ); diff --git a/test/bundler/transpiler/macro-test.test.ts b/test/bundler/transpiler/macro-test.test.ts index 16a41fa0d250..1c1fbd1b37a8 100644 --- a/test/bundler/transpiler/macro-test.test.ts +++ b/test/bundler/transpiler/macro-test.test.ts @@ -12,6 +12,7 @@ import defaultMacro, { identity as identity1, identity as identity2, ireturnapromise, + stringNumberAndBooleanPrototypes, symbolKeys, } from "./macro.ts" with { type: "macro" }; @@ -47,6 +48,11 @@ test("object with Symbol keys", () => { expect(Reflect.ownKeys(value)).toEqual(["v"]); }); +// In JSC these prototypes are boxed values of "", 0 and false. They inline as the objects they are. +test("String.prototype, Number.prototype and Boolean.prototype", () => { + expect(stringNumberAndBooleanPrototypes()).toEqual([{}, {}, {}]); +}); + test("escaping", () => { expect(identity("\\")).toBe("\\"); expect(identity("\f")).toBe("\f"); diff --git a/test/bundler/transpiler/macro.ts b/test/bundler/transpiler/macro.ts index c21c8768262f..46ac8e6f06f1 100644 --- a/test/bundler/transpiler/macro.ts +++ b/test/bundler/transpiler/macro.ts @@ -27,3 +27,7 @@ export async function ireturnapromise() { export function symbolKeys() { return { v: 2, [Symbol.for("s")]: 1, [Symbol("d")]: 3, [Symbol()]: 4 }; } + +export function stringNumberAndBooleanPrototypes() { + return [String.prototype, Number.prototype, Boolean.prototype]; +} diff --git a/test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts b/test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts index 383ce007d7a0..db958d56c28e 100644 --- a/test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts +++ b/test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts @@ -939,6 +939,51 @@ test("error inline snapshots", () => { }).toThrowErrorMatchingInlineSnapshot(`"😊"`); }); +// The snapshot printer has a property walk of its own. console.log prints the three prototypes as `{}` (#8627). +test("String.prototype, Number.prototype, Boolean.prototype and boxed values", () => { + class N extends Number {} + const withKeys = (value: object) => + Object.defineProperty(Object.assign(value, { b: 1, a: 2 }), "hidden", { value: 3 }); + expect(String.prototype).toMatchInlineSnapshot(`String {}`); + expect(Number.prototype).toMatchInlineSnapshot(` + Number { + "toExponential": [Function: toExponential], + "toFixed": [Function: toFixed], + "toLocaleString": [Function: toLocaleString], + "toPrecision": [Function: toPrecision], + "toString": [Function: toString], + "valueOf": [Function: valueOf], + } + `); + expect(Boolean.prototype).toMatchInlineSnapshot(` + Boolean { + "toString": [Function: toString], + "valueOf": [Function: valueOf], + } + `); + expect(withKeys(new Number(1))).toMatchInlineSnapshot(` + Number { + "a": 2, + "b": 1, + "hidden": 3, + } + `); + expect(withKeys(new Boolean(true))).toMatchInlineSnapshot(` + Boolean { + "a": 2, + "b": 1, + "hidden": 3, + } + `); + expect(withKeys(new N(1))).toMatchInlineSnapshot(` + N { + "a": 2, + "b": 1, + "hidden": 3, + } + `); +}); + test("snapshot numbering", () => { function fails() { throw new Error("snap"); diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index b3c80d53db96..c0563d5949d9 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -11,6 +11,7 @@ import { } from "harness"; import { join } from "path"; import util from "util"; +import { runInNewContext } from "vm"; it("prototype", () => { const prototypes = [ Request.prototype, @@ -1536,3 +1537,98 @@ it.skipIf(!isASAN)("prints a substring-rope jsx type after a prop's inspect.cust expect(stdout).toEndWith("\n"); expect(exitCode).toBe(0); }); + +// In JSC these three prototypes are boxed values of "", 0 and false. +describe("String.prototype, Number.prototype and Boolean.prototype", () => { + const prototypes = [String.prototype, Number.prototype, Boolean.prototype]; + + async function run(script) { + await using proc = Bun.spawn({ cmd: [bunExe(), "-e", script], env: bunEnv, stderr: "pipe" }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + return { stdout, stderr, exitCode }; + } + + it("print as an empty object at every position", () => { + expect(prototypes.map(prototype => Bun.inspect(prototype))).toEqual(["{}", "{}", "{}"]); + expect(prototypes.map(prototype => Bun.inspect(prototype, { sorted: true }))).toEqual(["{}", "{}", "{}"]); + expect(Bun.inspect({ s: String.prototype, n: Number.prototype, b: Boolean.prototype })).toBe( + "{\n s: {},\n n: {},\n b: {},\n}", + ); + expect(Bun.inspect({ a: { b: String.prototype } }, { depth: 1 })).toBe("{\n a: {\n b: [Object ...],\n },\n}"); + + const empty = {}; + expect(Bun.inspect([String.prototype])).toBe(Bun.inspect([empty])); + expect(Bun.inspect(new Map([[String.prototype, Number.prototype]]))).toBe(Bun.inspect(new Map([[empty, {}]]))); + expect(Bun.inspect({ x: String.prototype }, { colors: true })).toBe(Bun.inspect({ x: empty }, { colors: true })); + expect(Bun.inspect.table([{ a: String.prototype }])).toBe(Bun.inspect.table([{ a: empty }])); + }); + + it("a boxed value still prints as one", () => { + class N extends Number {} + class B extends Boolean {} + class S extends String {} + expect([new Number(1), new N(1), new Boolean(false), new B(true)].map(value => Bun.inspect(value))).toEqual([ + "[Number: 1]", + "[Number (N): 1]", + "[Boolean: false]", + "[Boolean (B): true]", + ]); + expect(Bun.inspect(new S("abc"))).toContain("abc"); + expect(Bun.inspect(new String("abc"))).toContain("abc"); + }); + + it("a boxed value that the object printer gets keeps its class name", () => { + class Id extends String {} + class N extends Number {} + const headers = new Headers(); + headers.toJSON = () => new N(3); + expect(Bun.inspect(expect.objectContaining(new Number(1)))).toStartWith("ObjectContaining Number {"); + expect(Bun.inspect(expect.objectContaining(new Id("ab")))).toStartWith( + 'ObjectContaining Id {\n "0": "a",\n "1": "b",\n length: 2,', + ); + expect(Bun.inspect(headers)).toStartWith("Headers N {"); + }); + + it("those of another realm print the same", () => { + const realm = runInNewContext("({ s: String.prototype, n: Number.prototype, b: Boolean.prototype })"); + expect([realm.s, realm.n, realm.b].map(value => Bun.inspect(value))).toEqual(["{}", "{}", "{}"]); + }); + + it.concurrent("print what a program puts on them, and no built-in member", async () => { + const script = ` + let calls = 0; + Number.prototype.toString = Number.prototype.valueOf = () => calls++; + Number.prototype.b = 1; + Number.prototype.a = 2; + Object.defineProperty(Boolean.prototype, "hidden", { value: 1 }); + Object.setPrototypeOf(Boolean.prototype, { inherited: 1 }); + console.log(Number.prototype, Boolean.prototype); + // Not sorted: the walk that sorts also lists the built-in methods. + console.log(Bun.inspect(Number.prototype, { sorted: true })); + console.log("calls:", calls); + `; + expect(await run(script)).toEqual({ + stdout: "{\n b: 1,\n a: 2,\n} {}\n{\n b: 1,\n a: 2,\n}\ncalls: 0\n", + stderr: "", + exitCode: 0, + }); + }); + + it.concurrent("print the same once JSC stores their methods as ordinary properties", async () => { + // Object.entries() makes JSC reify the lazy methods of a prototype. + // The quick property walk then finds each of them on the Structure. + const script = ` + const { hasNonReifiedStatic } = require("bun:internal-for-testing"); + Number.prototype.added = 1; + Object.entries(Number.prototype); + Object.entries(Boolean.prototype); + console.log(hasNonReifiedStatic(Number.prototype), hasNonReifiedStatic(Boolean.prototype)); + console.log(Number.prototype, Boolean.prototype); + `; + expect(await run(script)).toEqual({ + stdout: "false false\n{\n added: 1,\n} {}\n", + stderr: "", + exitCode: 0, + }); + }); +}); diff --git a/test/js/web/console/console-log.expected.txt b/test/js/web/console/console-log.expected.txt index 6b1d807fbec7..2f621996186f 100644 --- a/test/js/web/console/console-log.expected.txt +++ b/test/js/web/console/console-log.expected.txt @@ -20,6 +20,9 @@ Symbol(Symbol Description) [String: "Hello 👋🏼"] [Number: 5] [Boolean: true] +{} +{} {} +{} 1 [ 123, 456, 789 ] { name: "foo", diff --git a/test/js/web/console/console-log.js b/test/js/web/console/console-log.js index 91c3bddd0278..f58827eba7ab 100644 --- a/test/js/web/console/console-log.js +++ b/test/js/web/console/console-log.js @@ -18,6 +18,9 @@ console.log(new String("Hello")); console.log(new String("Hello 👋🏼")); console.log(new Number(5)); console.log(new Boolean(true)); +console.log(String.prototype); +console.log(Number.prototype, Boolean.prototype); +console.log(String.prototype, 1); console.log([123, 456, 789]); console.log({ name: "foo" }); console.log({ a: 123, b: 456, c: 789 }); From 1cb972df19dc1ef93d95f02801c0d91f7940c0cb Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 31 Oct 2026 18:40:09 +0000 Subject: [PATCH 2/4] console: an object that inherits from String.prototype does not list its methods The prototype walk of forEachPropertyImpl climbs up to 5 levels and stops only at Object.prototype and Function.prototype. So Object.create(String.prototype) printed the 52 members of String.prototype, and the same for Number.prototype and Boolean.prototype. The walk now does not climb into a boxed String, Number or Boolean, which these three prototypes are. The other built-in prototypes are not changed (#1713). --- src/jsc/bindings/bindings.cpp | 22 ++++++-- test/js/bun/util/inspect.test.js | 55 ++++++++++++++++---- test/js/web/console/console-log.expected.txt | 1 + test/js/web/console/console-log.js | 1 + 4 files changed, 66 insertions(+), 13 deletions(-) diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index ed668269ecbd..2d112b03de35 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -5554,6 +5554,22 @@ extern "C" void JSGlobalObject__throwStackOverflow(JSC::JSGlobalObject* globalOb throwStackOverflowError(globalObject, scope); } +// A boxed String, Number or Boolean. String.prototype, Number.prototype and Boolean.prototype are boxed values too. +static ALWAYS_INLINE bool isBoxedStringNumberOrBoolean(JSC::JSValue value) +{ + if (!value.isCell()) + return false; + switch (value.asCell()->type()) { + case StringObjectType: + case DerivedStringObjectType: + case NumberObjectType: + case BooleanObjectType: + return true; + default: + return false; + } +} + template static void JSC__JSValue__forEachPropertyImpl(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObject, void* arg2, void (*iter)(JSC::JSGlobalObject* arg0, void* ctx, EncodedSlice* arg2, JSC::EncodedJSValue JSValue3, bool isSymbol, bool isPrivateSymbol)) { @@ -5626,7 +5642,7 @@ static void JSC__JSValue__forEachPropertyImpl(JSC::EncodedJSValue JSValue0, JSC: JSValue proto = object->getPrototype(globalObject); RETURN_IF_EXCEPTION(scope, ); - if (proto) { + if (proto && !isBoxedStringNumberOrBoolean(proto)) { if ((structure = proto.structureOrNull())) { prototypeObject = proto; fast = canWalkQuickly(asObject(proto)); @@ -5734,7 +5750,7 @@ static void JSC__JSValue__forEachPropertyImpl(JSC::EncodedJSValue JSValue0, JSC: JSValue proto = prototypeObject.getPrototype(globalObject); RETURN_IF_EXCEPTION(scope, ); if (proto) { - if (!(proto == globalObject->objectPrototype() || proto == globalObject->functionPrototype() || (proto.inherits() && uncheckedDowncast(proto)->target() != globalObject))) { + if (!(proto == globalObject->objectPrototype() || proto == globalObject->functionPrototype() || isBoxedStringNumberOrBoolean(proto) || (proto.inherits() && uncheckedDowncast(proto)->target() != globalObject))) { if ((structure = proto.structureOrNull())) { prototypeObject = proto; fast = canWalkQuickly(asObject(proto)); @@ -5756,7 +5772,7 @@ static void JSC__JSValue__forEachPropertyImpl(JSC::EncodedJSValue JSValue0, JSC: auto* globalProxy = dynamicDowncast(object); const bool isOtherGlobal = globalProxy && globalProxy->target() != globalObject; - while (iterating && !(iterating == globalObject->objectPrototype() || iterating == globalObject->functionPrototype() || (iterating != object && iterating->inherits() && uncheckedDowncast(iterating)->target() != globalObject)) && prototypeCount++ < 5) { + while (iterating && !(iterating == globalObject->objectPrototype() || iterating == globalObject->functionPrototype() || (iterating != object && (isBoxedStringNumberOrBoolean(iterating) || (iterating->inherits() && uncheckedDowncast(iterating)->target() != globalObject)))) && prototypeCount++ < 5) { if constexpr (nonIndexedOnly) { iterating->getOwnNonIndexPropertyNames(globalObject, properties, DontEnumPropertiesMode::Include); } else { diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index c0563d5949d9..661276afc440 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -9,6 +9,7 @@ import { tmpdirSync, withoutAggressiveGC, } from "harness"; +import { openSync } from "fs"; import { join } from "path"; import util from "util"; import { runInNewContext } from "vm"; @@ -1582,16 +1583,49 @@ describe("String.prototype, Number.prototype and Boolean.prototype", () => { class N extends Number {} const headers = new Headers(); headers.toJSON = () => new N(3); - expect(Bun.inspect(expect.objectContaining(new Number(1)))).toStartWith("ObjectContaining Number {"); - expect(Bun.inspect(expect.objectContaining(new Id("ab")))).toStartWith( - 'ObjectContaining Id {\n "0": "a",\n "1": "b",\n length: 2,', + expect(Bun.inspect(expect.objectContaining(new Number(1)))).toBe("ObjectContaining Number {}"); + expect(Bun.inspect(expect.objectContaining(new Id("ab")))).toBe( + 'ObjectContaining Id {\n "0": "a",\n "1": "b",\n length: 2,\n}', + ); + expect(Bun.inspect(headers)).toBe("Headers N {}"); + }); + + it("an object that inherits from one does not list its methods", () => { + expect(prototypes.map(prototype => Bun.inspect(Object.create(prototype)))).toEqual([ + "String {}", + "Number {}", + "Boolean {}", + ]); + expect(Bun.inspect(Object.assign(Object.create(String.prototype), { x: 1 }))).toBe("String {\n x: 1,\n}"); + // An accessor takes the object off the quick property walk. + expect(Bun.inspect(Object.create(String.prototype, { x: { get: () => 1, enumerable: true } }))).toBe( + "String {\n x: [Getter],\n}", + ); + // A boxed value is not a prototype to list either. + expect(Bun.inspect(Object.create(new String("a")))).toBe("String {}"); + expect(Bun.inspect(Object.create(new Number(1)))).toBe("Number {}"); + + class S extends String {} + expect(Bun.inspect(S.prototype)).toBe("S {}"); + expect(Bun.inspect(Object.create(S.prototype))).toBe("S {}"); + }); + + it("an error message that prints such an object stays short", () => { + expect(() => openSync(import.meta.path, Object.create(String.prototype))).toThrow( + "The argument 'flags' is invalid. Received String {}", ); - expect(Bun.inspect(headers)).toStartWith("Headers N {"); }); it("those of another realm print the same", () => { - const realm = runInNewContext("({ s: String.prototype, n: Number.prototype, b: Boolean.prototype })"); - expect([realm.s, realm.n, realm.b].map(value => Bun.inspect(value))).toEqual(["{}", "{}", "{}"]); + const realm = runInNewContext( + "({ s: String.prototype, n: Number.prototype, b: Boolean.prototype, heir: Object.create(String.prototype) })", + ); + expect([realm.s, realm.n, realm.b, realm.heir].map(value => Bun.inspect(value))).toEqual([ + "{}", + "{}", + "{}", + "String {}", + ]); }); it.concurrent("print what a program puts on them, and no built-in member", async () => { @@ -1602,13 +1636,13 @@ describe("String.prototype, Number.prototype and Boolean.prototype", () => { Number.prototype.a = 2; Object.defineProperty(Boolean.prototype, "hidden", { value: 1 }); Object.setPrototypeOf(Boolean.prototype, { inherited: 1 }); - console.log(Number.prototype, Boolean.prototype); + console.log(Number.prototype, Boolean.prototype, Object.create(Number.prototype)); // Not sorted: the walk that sorts also lists the built-in methods. console.log(Bun.inspect(Number.prototype, { sorted: true })); console.log("calls:", calls); `; expect(await run(script)).toEqual({ - stdout: "{\n b: 1,\n a: 2,\n} {}\n{\n b: 1,\n a: 2,\n}\ncalls: 0\n", + stdout: "{\n b: 1,\n a: 2,\n} {} Number {}\n{\n b: 1,\n a: 2,\n}\ncalls: 0\n", stderr: "", exitCode: 0, }); @@ -1623,10 +1657,11 @@ describe("String.prototype, Number.prototype and Boolean.prototype", () => { Object.entries(Number.prototype); Object.entries(Boolean.prototype); console.log(hasNonReifiedStatic(Number.prototype), hasNonReifiedStatic(Boolean.prototype)); - console.log(Number.prototype, Boolean.prototype); + console.log(Number.prototype, Boolean.prototype, Object.create(Boolean.prototype)); + console.log(Object.assign(Object.create(Boolean.prototype), { own: 1 })); `; expect(await run(script)).toEqual({ - stdout: "false false\n{\n added: 1,\n} {}\n", + stdout: "false false\n{\n added: 1,\n} {} Boolean {}\nBoolean {\n own: 1,\n}\n", stderr: "", exitCode: 0, }); diff --git a/test/js/web/console/console-log.expected.txt b/test/js/web/console/console-log.expected.txt index 2f621996186f..b23adcd8dab1 100644 --- a/test/js/web/console/console-log.expected.txt +++ b/test/js/web/console/console-log.expected.txt @@ -22,6 +22,7 @@ Symbol(Symbol Description) [Boolean: true] {} {} {} +String {} {} 1 [ 123, 456, 789 ] { diff --git a/test/js/web/console/console-log.js b/test/js/web/console/console-log.js index f58827eba7ab..3cb152fe6133 100644 --- a/test/js/web/console/console-log.js +++ b/test/js/web/console/console-log.js @@ -20,6 +20,7 @@ console.log(new Number(5)); console.log(new Boolean(true)); console.log(String.prototype); console.log(Number.prototype, Boolean.prototype); +console.log(Object.create(String.prototype)); console.log(String.prototype, 1); console.log([123, 456, 789]); console.log({ name: "foo" }); From 20f194e3f09b5ea280feeb9909d4b96d7d800df7 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:47:50 +0000 Subject: [PATCH 3/4] [autofix.ci] apply automated fixes --- test/js/bun/util/inspect.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index 661276afc440..198d347aa733 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -1,4 +1,5 @@ import { describe, expect, it } from "bun:test"; +import { openSync } from "fs"; import { bunEnv, bunExe, @@ -9,7 +10,6 @@ import { tmpdirSync, withoutAggressiveGC, } from "harness"; -import { openSync } from "fs"; import { join } from "path"; import util from "util"; import { runInNewContext } from "vm"; From e6782c2aa0d1410d072fa72e507eb9e7c0d2987c Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:22:44 +0000 Subject: [PATCH 4/4] test: assert the exact text of a boxed String and of an instance of a String subclass --- test/js/bun/util/inspect.test.js | 4 ++-- test/js/web/console/console-log.expected.txt | 1 + test/js/web/console/console-log.js | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index 198d347aa733..4857e698c64e 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -1574,8 +1574,8 @@ describe("String.prototype, Number.prototype and Boolean.prototype", () => { "[Boolean: false]", "[Boolean (B): true]", ]); - expect(Bun.inspect(new S("abc"))).toContain("abc"); - expect(Bun.inspect(new String("abc"))).toContain("abc"); + // Bun.inspect() prints a boxed String as a plain string. console-log.js has the form that console.log() prints. + expect([new String("abc"), new S("abc")].map(value => Bun.inspect(value))).toEqual(['"abc"', '"abc"']); }); it("a boxed value that the object printer gets keeps its class name", () => { diff --git a/test/js/web/console/console-log.expected.txt b/test/js/web/console/console-log.expected.txt index b23adcd8dab1..c2082742472a 100644 --- a/test/js/web/console/console-log.expected.txt +++ b/test/js/web/console/console-log.expected.txt @@ -24,6 +24,7 @@ Symbol(Symbol Description) {} {} String {} {} 1 +[String: "abc"] [ 123, 456, 789 ] { name: "foo", diff --git a/test/js/web/console/console-log.js b/test/js/web/console/console-log.js index 3cb152fe6133..5db7e70bf118 100644 --- a/test/js/web/console/console-log.js +++ b/test/js/web/console/console-log.js @@ -22,6 +22,7 @@ console.log(String.prototype); console.log(Number.prototype, Boolean.prototype); console.log(Object.create(String.prototype)); console.log(String.prototype, 1); +console.log(new (class S extends String {})("abc")); console.log([123, 456, 789]); console.log({ name: "foo" }); console.log({ a: 123, b: 456, c: 789 });