Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 52 additions & 1 deletion src/jsc/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4899,6 +4899,37 @@ void JSC__VM__releaseWeakRefs(JSC::VM* arg0)
arg0->finalizeSynchronousJSExecution();
}

// 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)
{
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 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.
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<JSC::JSFunction>(constructor) : nullptr;
}

void JSC__JSValue__getClassName(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, ZigString* arg2)
{
JSValue value = JSValue::decode(JSValue0);
Expand All @@ -4919,6 +4950,15 @@ void JSC__JSValue__getClassName(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObjec

JSObject* obj = value.toObject(arg1);

if (JSC::JSFunction* constructor = constructorForClassName(arg1->vm(), obj)) {
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;
}
}

auto calculated = JSObject::calculatedClassName(obj);
if (calculated.length() > 0) {
*arg2 = Zig::toZigString(calculated);
Expand Down Expand Up @@ -4961,6 +5001,11 @@ void JSC__JSValue__getNameProperty(JSC::EncodedJSValue JSValue0, JSC::JSGlobalOb
}

if (JSC::JSFunction* function = dynamicDowncast<JSC::JSFunction>(obj)) {
WTF::String redefinedName = redefinedFunctionName(vm, function);
if (!redefinedName.isNull()) {
*arg2 = Zig::toZigString(redefinedName);
return;
}

WTF::String actualName = function->name(vm);
if (!actualName.isEmpty() || function->isHostOrBuiltinFunction()) {
Expand Down Expand Up @@ -4992,7 +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();
auto displayName = JSC::getCalculatedDisplayName(vm, object);
WTF::String displayName;
if (JSC::JSFunction* function = dynamicDowncast<JSC::JSFunction>(object)) {
displayName = redefinedFunctionName(vm, function);
}
if (displayName.isNull()) {
displayName = JSC::getCalculatedDisplayName(vm, object);
}

// JSC doesn't include @@toStringTag in calculated display name
if (displayName.isEmpty()) {
Expand Down
71 changes: 71 additions & 0 deletions test/js/bun/util/inspect.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,77 @@ 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("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", () => {
const blob = new Blob(["foo"], { type: "text/plain" });
expect(Bun.inspect(blob)).toBe('Blob (3 bytes) {\n type: "text/plain;charset=utf-8"\n}');
Expand Down