From 4abb97dfdd42952f725ffa54b2aa5c16968165b5 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 15 Aug 2026 07:00:20 +0000 Subject: [PATCH 1/4] inspect: display a name defined on a function or class with Object.defineProperty console.log and Bun.inspect named functions, classes and class instances from the executable (JSC's calculatedDisplayName / calculatedClassName), so a class whose name was set with Object.defineProperty, as tsc's __setFunctionName and the __name helper do for lowered decorated classes, printed as its temporary binding: `[class _classThis]`, `_classThis {}`. Node prints the defined name. Check for an own string `name` data property first, on the function itself and on the constructor an instance or prototype object is named after; a `displayName` property keeps its precedence and accessors are not run. --- src/jsc/bindings/bindings.cpp | 53 ++++++++++++++++++++++++++++++++ test/js/bun/util/inspect.test.js | 49 +++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index 522e323496ad..4c2f2c8e3ce0 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -4899,6 +4899,38 @@ void JSC__VM__releaseWeakRefs(JSC::VM* arg0) arg0->finalizeSynchronousJSExecution(); } +// A "name" redefined with Object.defineProperty (tsc's __setFunctionName, esbuild's and Bun's +// __name) is what node displays; JSC's calculatedDisplayName() only looks at "displayName" and +// the executable. "displayName" keeps its precedence. +static WTF::String explicitFunctionName(JSC::VM& vm, JSC::JSFunction* function) +{ + if (!function->displayName(vm).isEmpty()) { + return WTF::String(); + } + + JSC::JSValue name = function->getDirect(vm, vm.propertyNames->name); + if (name && JSC::isJSString(name)) { + return JSC::asString(name)->tryGetValue(); + } + + return WTF::String(); +} + +// The function JSObject::calculatedClassName() would name an object after: its own +// "constructor" (prototype objects) or its prototype's (instances). +static JSC::JSFunction* constructorForClassName(JSC::VM& vm, JSC::JSObject* object) +{ + JSC::JSValue constructor = object->getDirect(vm, vm.propertyNames->constructor); + if (!constructor && !object->structure()->typeInfo().overridesGetPrototype()) { + JSC::JSValue prototype = object->getPrototypeDirect(); + if (prototype.isObject()) { + constructor = JSC::asObject(prototype)->getDirect(vm, vm.propertyNames->constructor); + } + } + + return constructor ? dynamicDowncast(constructor) : nullptr; +} + void JSC__JSValue__getClassName(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, ZigString* arg2) { JSValue value = JSValue::decode(JSValue0); @@ -4919,6 +4951,14 @@ void JSC__JSValue__getClassName(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObjec JSObject* obj = value.toObject(arg1); + if (JSC::JSFunction* constructor = constructorForClassName(arg1->vm(), obj)) { + auto explicitName = explicitFunctionName(arg1->vm(), constructor); + if (!explicitName.isEmpty()) { + *arg2 = Zig::toZigString(explicitName); + return; + } + } + auto calculated = JSObject::calculatedClassName(obj); if (calculated.length() > 0) { *arg2 = Zig::toZigString(calculated); @@ -4961,6 +5001,11 @@ void JSC__JSValue__getNameProperty(JSC::EncodedJSValue JSValue0, JSC::JSGlobalOb } if (JSC::JSFunction* function = dynamicDowncast(obj)) { + WTF::String explicitName = explicitFunctionName(vm, function); + if (!explicitName.isEmpty()) { + *arg2 = Zig::toZigString(explicitName); + return; + } WTF::String actualName = function->name(vm); if (!actualName.isEmpty() || function->isHostOrBuiltinFunction()) { @@ -4992,6 +5037,14 @@ void JSC__JSValue__getNameProperty(JSC::EncodedJSValue JSValue0, JSC::JSGlobalOb auto& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); JSObject* object = value.getObject(); + if (JSC::JSFunction* function = dynamicDowncast(object)) { + auto explicitName = explicitFunctionName(vm, function); + if (!explicitName.isEmpty()) { + *arg2 = Bun::toStringRef(explicitName); + return; + } + } + auto displayName = JSC::getCalculatedDisplayName(vm, object); // JSC doesn't include @@toStringTag in calculated display name diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index 5c91f3dbe05f..a75ae63b1427 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -617,6 +617,55 @@ describe("console.logging class displays names and extends", async () => { } }); +describe("a name defined with Object.defineProperty is displayed", () => { + // tsc (__setFunctionName) and esbuild/Bun (__name) name the anonymous class their decorator + // lowering produces this way; the class binding they assign to is a temporary. + function setName(value, name) { + return Object.defineProperty(value, "name", { value: name, configurable: true }); + } + class Base {} + const Item = setName(class {}, "Item"); + const Derived = setName(class extends Base {}, "Derived"); + + it("class and instances", () => { + expect([Bun.inspect(Item), Bun.inspect(new Item()), Bun.inspect({ item: new Item() }, { compact: true })]).toEqual([ + "[class Item]", + "Item {}", + "{ item: Item {} }", + ]); + }); + + it("extends clause and subclass instances", () => { + class Child extends Item {} + expect([Bun.inspect(Derived), Bun.inspect(new Derived()), Bun.inspect(Child), Bun.inspect(Item.prototype)]).toEqual( + ["[class Derived extends Base]", "Derived {}", "[class Child extends Item]", "Item {}"], + ); + }); + + it("functions", () => { + expect(Bun.inspect(setName(function original() {}, "renamed"))).toBe("[Function: renamed]"); + }); + + it("displayName still takes precedence", () => { + const fn = setName(function original() {}, "renamed"); + fn.displayName = "Display"; + expect(Bun.inspect(fn)).toBe("[Function: Display]"); + }); + + it("a name accessor is not invoked", () => { + let called = false; + const Cls = Object.defineProperty(class Original {}, "name", { + get() { + called = true; + return "from getter"; + }, + }); + expect(Bun.inspect(Cls)).toBe("[class Original]"); + expect(Bun.inspect(new Cls())).toBe("Original {}"); + expect(called).toBe(false); + }); +}); + it("console.log on a Blob shows name", () => { const blob = new Blob(["foo"], { type: "text/plain" }); expect(Bun.inspect(blob)).toBe('Blob (3 bytes) {\n type: "text/plain;charset=utf-8"\n}'); From 65ad881ba5762df7e4423523b5b3ce4c84a89d27 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 15 Aug 2026 07:09:15 +0000 Subject: [PATCH 2/4] inspect: shorten helper comments --- src/jsc/bindings/bindings.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index 4c2f2c8e3ce0..01c6d206c4e8 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -4899,9 +4899,7 @@ void JSC__VM__releaseWeakRefs(JSC::VM* arg0) arg0->finalizeSynchronousJSExecution(); } -// A "name" redefined with Object.defineProperty (tsc's __setFunctionName, esbuild's and Bun's -// __name) is what node displays; JSC's calculatedDisplayName() only looks at "displayName" and -// the executable. "displayName" keeps its precedence. +// A "name" set with Object.defineProperty, which JSC's calculatedDisplayName() does not consult. static WTF::String explicitFunctionName(JSC::VM& vm, JSC::JSFunction* function) { if (!function->displayName(vm).isEmpty()) { @@ -4916,8 +4914,7 @@ static WTF::String explicitFunctionName(JSC::VM& vm, JSC::JSFunction* function) return WTF::String(); } -// The function JSObject::calculatedClassName() would name an object after: its own -// "constructor" (prototype objects) or its prototype's (instances). +// The constructor JSObject::calculatedClassName() names an object after. static JSC::JSFunction* constructorForClassName(JSC::VM& vm, JSC::JSObject* object) { JSC::JSValue constructor = object->getDirect(vm, vm.propertyNames->constructor); From 70537f60746bdae2d986ed074ae9943f0fbd7b41 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 15 Aug 2026 12:26:35 +0000 Subject: [PATCH 3/4] ci: retrigger From 5727b0f7a3b1322a6629fc83ee72352b1f138b8e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 15 Aug 2026 13:12:07 +0000 Subject: [PATCH 4/4] inspect: only use a name the program redefined, including an empty one Key the lookup on JSC's own record of the name property having been modified (FunctionRareData::hasModifiedNameForBoundOrNonHostFunction) instead of on the presence of the property: JSC creates the property lazily the first time .name is read, so reading it must not change how a getter or an export default prints. With presence no longer the signal, a name redefined as "" can be honored too and prints the way an anonymous function or class instance already does. --- src/jsc/bindings/bindings.cpp | 39 ++++++++++++++++---------------- test/js/bun/util/inspect.test.js | 22 ++++++++++++++++++ 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index 01c6d206c4e8..154e922f3025 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -4899,19 +4899,21 @@ void JSC__VM__releaseWeakRefs(JSC::VM* arg0) arg0->finalizeSynchronousJSExecution(); } -// A "name" set with Object.defineProperty, which JSC's calculatedDisplayName() does not consult. -static WTF::String explicitFunctionName(JSC::VM& vm, JSC::JSFunction* function) +// Null unless the program redefined "name" (tsc's __setFunctionName, __name); a name JSC merely reified stays as printed today. +static WTF::String redefinedFunctionName(JSC::VM& vm, JSC::JSFunction* function) { - if (!function->displayName(vm).isEmpty()) { + JSC::FunctionRareData* rareData = function->rareData(); + if (!rareData || !rareData->hasModifiedNameForBoundOrNonHostFunction() || !function->displayName(vm).isEmpty()) { return WTF::String(); } JSC::JSValue name = function->getDirect(vm, vm.propertyNames->name); - if (name && JSC::isJSString(name)) { - return JSC::asString(name)->tryGetValue(); + if (!name || !JSC::isJSString(name)) { + return WTF::String(); } - return WTF::String(); + // Empty when the program made the function anonymous; callers print it like any anonymous function. + return JSC::asString(name)->tryGetValue(); } // The constructor JSObject::calculatedClassName() names an object after. @@ -4949,9 +4951,10 @@ void JSC__JSValue__getClassName(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObjec JSObject* obj = value.toObject(arg1); if (JSC::JSFunction* constructor = constructorForClassName(arg1->vm(), obj)) { - auto explicitName = explicitFunctionName(arg1->vm(), constructor); - if (!explicitName.isEmpty()) { - *arg2 = Zig::toZigString(explicitName); + auto redefinedName = redefinedFunctionName(arg1->vm(), constructor); + if (!redefinedName.isNull()) { + // Empty: same fallback as an instance of an anonymous class takes below. + *arg2 = redefinedName.isEmpty() ? Zig::toZigString(view) : Zig::toZigString(redefinedName); return; } } @@ -4998,9 +5001,9 @@ void JSC__JSValue__getNameProperty(JSC::EncodedJSValue JSValue0, JSC::JSGlobalOb } if (JSC::JSFunction* function = dynamicDowncast(obj)) { - WTF::String explicitName = explicitFunctionName(vm, function); - if (!explicitName.isEmpty()) { - *arg2 = Zig::toZigString(explicitName); + WTF::String redefinedName = redefinedFunctionName(vm, function); + if (!redefinedName.isNull()) { + *arg2 = Zig::toZigString(redefinedName); return; } @@ -5034,15 +5037,13 @@ void JSC__JSValue__getNameProperty(JSC::EncodedJSValue JSValue0, JSC::JSGlobalOb auto& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); JSObject* object = value.getObject(); + WTF::String displayName; if (JSC::JSFunction* function = dynamicDowncast(object)) { - auto explicitName = explicitFunctionName(vm, function); - if (!explicitName.isEmpty()) { - *arg2 = Bun::toStringRef(explicitName); - return; - } + displayName = redefinedFunctionName(vm, function); + } + if (displayName.isNull()) { + displayName = JSC::getCalculatedDisplayName(vm, object); } - - auto displayName = JSC::getCalculatedDisplayName(vm, object); // JSC doesn't include @@toStringTag in calculated display name if (displayName.isEmpty()) { diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index a75ae63b1427..2212b4251221 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -664,6 +664,28 @@ describe("a name defined with Object.defineProperty is displayed", () => { expect(Bun.inspect(new Cls())).toBe("Original {}"); expect(called).toBe(false); }); + + it("a name redefined as empty prints like an anonymous function", () => { + const Unnamed = setName(class Original {}, ""); + class Child extends Unnamed {} + expect([ + Bun.inspect(Unnamed), + Bun.inspect(new Unnamed()), + Bun.inspect({ u: new Unnamed() }, { compact: true }), + Bun.inspect(Child), + Bun.inspect(setName(function original() {}, "")), + ]).toEqual(["[class (anonymous)]", "{}", "{ u: {} }", "[class Child]", "[Function]"]); + }); + + it("reading .name does not change the output of an untouched function", () => { + const { get } = Object.getOwnPropertyDescriptor({ get accessor() {} }, "accessor"); + const Cls = class {}; + const holder = { key: function () {} }; + const values = [get, Cls, new Cls(), holder.key]; + const before = values.map(value => Bun.inspect(value)); + expect([get.name, Cls.name, holder.key.name]).toEqual(["get accessor", "Cls", "key"]); + expect(values.map(value => Bun.inspect(value))).toEqual(before); + }); }); it("console.log on a Blob shows name", () => {