From 35f92b3f09bf31a32e373b8c80a6d8af1bf6f8e2 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 14 Aug 2026 13:42:50 +0000 Subject: [PATCH 1/3] Name anonymous export default functions and classes "default" in stack traces and inspect JSC binds an anonymous `export default` to the private `*default*` identifier, whose string form is "starDefault". JSFunction::reifyName maps it to "default" when the `name` property is materialized, but the fallbacks Bun uses when that has not happened yet (ecmaName() in ErrorStackTrace.cpp, JSC::getCalculatedDisplayName, and the executable name in JSC__JSValue__getNameProperty) returned the raw identifier, so a frame rendered as `at new starDefault (...)` until something read `.name`, and Bun.inspect printed `[class starDefault]`. Route every one of those fallbacks through one helper that renders the `*default*` identifier as "default", and share the displayName/name chain between the regular and the finalizer-safe frame name lookups instead of keeping two copies of it. --- src/jsc/bindings/ErrorStackTrace.cpp | 143 ++++++++++++--------------- src/jsc/bindings/ErrorStackTrace.h | 11 +++ src/jsc/bindings/bindings.cpp | 5 +- test/js/bun/test/stack.test.ts | 121 ++++++++++++++++++++++- test/js/bun/util/inspect.test.js | 36 ++++++- 5 files changed, 230 insertions(+), 86 deletions(-) diff --git a/src/jsc/bindings/ErrorStackTrace.cpp b/src/jsc/bindings/ErrorStackTrace.cpp index 7afd14d4b236..681adddf26a2 100644 --- a/src/jsc/bindings/ErrorStackTrace.cpp +++ b/src/jsc/bindings/ErrorStackTrace.cpp @@ -524,6 +524,58 @@ String sourceURL(JSC::VM& vm, JSC::JSFunction* function) return Zig::sourceURL(function->jsExecutable()->source()); } +String functionNameForDisplay(JSC::VM& vm, const JSC::Identifier& name) +{ + if (name == vm.propertyNames->starDefaultPrivateName) { + return vm.propertyNames->defaultKeyword.string(); + } + + return name.string(); +} + +// Own data properties only: this also runs from error finalizers, where getters must not run +// and nothing may be allocated on the JS heap. +static String ownStringPropertyWithoutGC(JSC::JSObject* object, const JSC::Identifier& propertyName) +{ + unsigned attributes; + PropertyOffset offset = object->structure()->getConcurrently(propertyName.impl(), attributes); + if (offset == invalidOffset || (attributes & (PropertyAttribute::Accessor | PropertyAttribute::CustomAccessorOrValue))) { + return String(); + } + + JSValue value = object->getDirect(offset); + if (!value || !value.isString()) { + return String(); + } + + return asString(value)->tryGetValueWithoutGC(); +} + +String calculatedDisplayName(JSC::VM& vm, JSC::JSObject* object) +{ + auto jstype = object->type(); + if (jstype != JSC::JSFunctionType && jstype != JSC::InternalFunctionType) { + return emptyString(); + } + + String displayName = ownStringPropertyWithoutGC(object, vm.propertyNames->displayName); + if (!displayName.isEmpty()) { + return displayName; + } + + if (jstype == JSC::InternalFunctionType) { + return uncheckedDowncast(object)->name(); + } + + auto* function = uncheckedDowncast(object); + String name = function->nameWithoutGC(vm); + if (!name.isEmpty() || function->isHostFunction()) { + return name; + } + + return functionNameForDisplay(vm, function->jsExecutable()->ecmaName()); +} + String functionName(JSC::VM& vm, JSC::CodeBlock* codeBlock) { auto codeType = codeBlock->codeType(); @@ -534,7 +586,7 @@ String functionName(JSC::VM& vm, JSC::CodeBlock* codeBlock) } if (codeType == JSC::FunctionCode) { - return uncheckedDowncast(executable)->ecmaName().string(); + return functionNameForDisplay(vm, uncheckedDowncast(executable)->ecmaName()); } return String(); @@ -542,9 +594,7 @@ String functionName(JSC::VM& vm, JSC::CodeBlock* codeBlock) String functionName(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSObject* object) { - WTF::String functionName; - auto jstype = object->type(); - if (jstype == JSC::ProxyObjectType) return {}; + if (object->type() == JSC::ProxyObjectType) return {}; // First try the "name" property. { @@ -567,30 +617,7 @@ String functionName(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC:: } } - { - // Then try the "displayName" property (what this does internally) - auto topExceptionScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); - functionName = JSC::getCalculatedDisplayName(vm, object); - if (topExceptionScope.exception()) [[unlikely]] { - (void)topExceptionScope.tryClearException(); - } - } - - { - if (functionName.isEmpty()) { - if (jstype == JSC::JSFunctionType) { - auto* function = uncheckedDowncast(object); - functionName = function->nameWithoutGC(vm); - if (functionName.isEmpty() && !function->isHostFunction()) { - functionName = function->jsExecutable()->ecmaName().string(); - } - } else if (jstype == JSC::InternalFunctionType) { - functionName = uncheckedDowncast(object)->name(); - } - } - } - - return functionName; + return calculatedDisplayName(vm, object); } String functionName(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, const JSC::StackFrame& frame, FinalizerSafety finalizerSafety, unsigned int* flags) @@ -601,63 +628,17 @@ String functionName(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, const if (auto* callee = frame.callee()) { if (auto* object = callee->getObject()) { auto jstype = object->type(); - Structure* structure = object->structure(); - - auto setTypeFlagsIfNecessary = [&]() { - if (flags) { - if (jstype == JSC::JSFunctionType || jstype == JSC::InternalFunctionType) { - *flags |= static_cast(FunctionNameFlags::Function); - } - } - }; - - // First try the "name" property. - { - unsigned attributes; - PropertyOffset offset = structure->getConcurrently(vm.propertyNames->name.impl(), attributes); - if (offset != invalidOffset && !(attributes & (PropertyAttribute::Accessor | PropertyAttribute::CustomAccessorOrValue))) { - JSValue name = object->getDirect(offset); - if (name && name.isString()) { - auto str = asString(name)->tryGetValueWithoutGC(); - if (!str->isEmpty()) { - setTypeFlagsIfNecessary(); - return str; - } - } - } + if (flags && (jstype == JSC::JSFunctionType || jstype == JSC::InternalFunctionType)) { + *flags |= static_cast(FunctionNameFlags::Function); } - // Then try the "displayName" property. - { - unsigned attributes; - PropertyOffset offset = structure->getConcurrently(vm.propertyNames->displayName.impl(), attributes); - if (offset != invalidOffset && !(attributes & (PropertyAttribute::Accessor | PropertyAttribute::CustomAccessorOrValue))) { - JSValue name = object->getDirect(offset); - if (name && name.isString()) { - auto str = asString(name)->tryGetValueWithoutGC(); - if (!str->isEmpty()) { - setTypeFlagsIfNecessary(); - return str; - } - } - } + // First try the "name" property. + String name = ownStringPropertyWithoutGC(object, vm.propertyNames->name); + if (!name.isEmpty()) { + return name; } - // Lastly, try type-specific properties. - if (jstype == JSC::JSFunctionType) { - auto* function = uncheckedDowncast(object); - auto str = function->nameWithoutGC(vm); - if (str.isEmpty() && !function->isHostFunction()) { - setTypeFlagsIfNecessary(); - return function->jsExecutable()->ecmaName().string(); - } - setTypeFlagsIfNecessary(); - return str; - } else if (jstype == JSC::InternalFunctionType) { - auto str = uncheckedDowncast(object)->name(); - setTypeFlagsIfNecessary(); - return str; - } + return calculatedDisplayName(vm, object); } } diff --git a/src/jsc/bindings/ErrorStackTrace.h b/src/jsc/bindings/ErrorStackTrace.h index 17c9dc6822ea..8eb1df5f62a4 100644 --- a/src/jsc/bindings/ErrorStackTrace.h +++ b/src/jsc/bindings/ErrorStackTrace.h @@ -235,6 +235,17 @@ class FunctionNameFlags { static constexpr unsigned AddNewKeyword = 1 << 4; }; +// The name JSC recorded for a function, as it should be shown to users. An anonymous +// `export default` is bound to the private `*default*` identifier, which stringifies as +// "starDefault"; its reified `name` property is "default" (JSFunction::reifyName). +String functionNameForDisplay(JSC::VM& vm, const JSC::Identifier& name); + +// Bun's counterpart of JSC::getCalculatedDisplayName: a non-empty own "displayName" property, else +// the function's own name, else (for JS functions) its ecmaName via functionNameForDisplay. Empty +// for anything that is not a function. Runs no JS and allocates nothing on the JS heap, so it is +// also used to format stack traces from error finalizers. +String calculatedDisplayName(JSC::VM& vm, JSC::JSObject* object); + String functionName(JSC::VM& vm, JSC::CodeBlock* codeBlock); String functionName(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSObject* callee); String functionName(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, const JSC::StackFrame& frame, FinalizerSafety, unsigned int* flags); diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index 40e19b3e571e..15b757f07e7a 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -4957,7 +4957,8 @@ void JSC__JSValue__getNameProperty(JSC::EncodedJSValue JSValue0, JSC::JSGlobalOb return; } - actualName = function->jsExecutable()->name().string(); + // JSFunction::name() returns "" for the `*default*` binding of an anonymous `export default`. + actualName = Zig::functionNameForDisplay(vm, function->jsExecutable()->name()); *arg2 = Zig::toZigString(actualName); return; @@ -4981,7 +4982,7 @@ 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); + auto displayName = Zig::calculatedDisplayName(vm, object); // JSC doesn't include @@toStringTag in calculated display name if (displayName.isEmpty()) { diff --git a/test/js/bun/test/stack.test.ts b/test/js/bun/test/stack.test.ts index 63b28630a3f6..c265fad37923 100644 --- a/test/js/bun/test/stack.test.ts +++ b/test/js/bun/test/stack.test.ts @@ -1,6 +1,6 @@ import { $ } from "bun"; -import { expect, test } from "bun:test"; -import { bunEnv, bunExe, bunRun, normalizeBunSnapshot } from "harness"; +import { describe, expect, test } from "bun:test"; +import { bunEnv, bunExe, bunRun, normalizeBunSnapshot, tempDir } from "harness"; import { join } from "node:path"; test("name property is used for function calls in Error.stack", () => { @@ -149,3 +149,120 @@ test("Async functions frame should be included in stack trace", async () => { at async (file:NN:NN)" `); }); + +// JSC binds an anonymous `export default` to the internal `*default*` identifier and only maps it +// to the observable name "default" when the `name` property is reified. Frame names must say +// "default" too (as node does), whether or not something has read `.name` yet. +describe("anonymous export default is named 'default'", () => { + const modules = { + "class.mjs": `export default class { constructor() { this.stack = new Error("class").stack; } }`, + "subclass.mjs": ` + class Base { constructor() { this.stack = new Error("subclass").stack; } } + export default class extends Base {} + `, + "arrow.mjs": `export default () => new Error("arrow").stack;`, + "expression.mjs": `export default (function () { return new Error("expression").stack; });`, + // A function really called starDefault keeps its name; only the internal binding is renamed. + "named-star-default.mjs": `export default function starDefault() { return new Error("starDefault").stack; }`, + "throws.mjs": `export default class { constructor() { throw new Error("thrown by an anonymous default export"); } }`, + "uncaught.mjs": ` + import Thrower from "./throws.mjs"; + new Thrower(); + `, + "unhandled-rejection.mjs": ` + import Thrower from "./throws.mjs"; + Promise.resolve().then(() => new Thrower()); + `, + "main.mjs": ` + import Class from "./class.mjs"; + import Subclass from "./subclass.mjs"; + import arrow from "./arrow.mjs"; + import expression from "./expression.mjs"; + import starDefault from "./named-star-default.mjs"; + + const frameNames = (stack, count) => + stack + .split("\\n") + .slice(1, 1 + count) + .map(line => line.trim().replace(/^at /, "").replace(/ \\(.*$/, "")); + + const topCallSite = getStack => { + const previous = Error.prepareStackTrace; + Error.prepareStackTrace = (_, callSites) => [callSites[0].getFunctionName(), callSites[0].isConstructor()]; + try { + return getStack(); + } finally { + Error.prepareStackTrace = previous; + } + }; + + const observe = () => ({ + class: frameNames(new Class().stack, 1), + subclass: frameNames(new Subclass().stack, 2), + arrow: frameNames(arrow(), 1), + expression: frameNames(expression(), 1), + starDefault: frameNames(starDefault(), 1), + callSites: [topCallSite(() => new Class().stack), topCallSite(arrow)], + }); + + const beforeReadingName = observe(); + const names = [Class.name, Subclass.name, arrow.name, expression.name, starDefault.name]; + const afterReadingName = observe(); + console.log(JSON.stringify({ beforeReadingName, names, afterReadingName })); + `, + }; + + const expected = { + class: ["new default"], + subclass: ["new Base", "new default"], + arrow: ["default"], + expression: ["default"], + starDefault: ["starDefault"], + callSites: [ + ["default", true], + ["default", false], + ], + }; + + test.concurrent("error.stack and CallSite#getFunctionName", async () => { + using dir = tempDir("export-default-name", modules); + await using proc = Bun.spawn({ + cmd: [bunExe(), "main.mjs"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ result: JSON.parse(stdout), stderr, exitCode }).toEqual({ + result: { + beforeReadingName: expected, + names: ["default", "default", "default", "default", "starDefault"], + afterReadingName: expected, + }, + stderr: "", + exitCode: 0, + }); + }); + + // Bun's own error printer names frames through a separate, GC-safe lookup from error.stack. + test.concurrent.each([ + ["uncaught.mjs", ["at new default (file:NN:NN)", "at /uncaught.mjs"]], + ["unhandled-rejection.mjs", ["at new default (file:NN:NN)", "at (file:NN:NN)"]], + ])("frames printed for an uncaught error: %s", async (entry, frames) => { + using dir = tempDir("export-default-name", modules); + await using proc = Bun.spawn({ + cmd: [bunExe(), entry], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + const printedFrames = normalizeBunSnapshot(stderr, String(dir)) + .split("\n") + .map(line => line.trim().replace(/:\d+:\d+$/, "")) + .filter(line => line.startsWith("at ")); + expect({ stdout, printedFrames, exitCode }).toEqual({ stdout: "", printedFrames: frames, exitCode: 1 }); + }); +}); diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index 5c91f3dbe05f..a7d88210ae4b 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -1,5 +1,5 @@ import { describe, expect, it } from "bun:test"; -import { bunEnv, bunExe, isASAN, isWindows, normalizeBunSnapshot, tmpdirSync } from "harness"; +import { bunEnv, bunExe, isASAN, isWindows, normalizeBunSnapshot, tempDir, tmpdirSync } from "harness"; import { join } from "path"; import util from "util"; it("prototype", () => { @@ -617,6 +617,40 @@ describe("console.logging class displays names and extends", async () => { } }); +// JSC names an anonymous `export default` after its internal `*default*` binding; `.name` is +// "default", and so is what node prints. +it("anonymous export default class and function are named 'default'", async () => { + using dir = tempDir("inspect-export-default", { + "class.mjs": `export default class {}`, + "subclass.mjs": ` + export class Base {} + export default class extends Base {} + `, + "arrow.mjs": `export default () => {};`, + "main.mjs": ` + import Class from "./class.mjs"; + import Subclass from "./subclass.mjs"; + import arrow from "./arrow.mjs"; + + const element = { $$typeof: Symbol.for("react.element"), type: Class, props: {}, key: null }; + console.log(JSON.stringify([Class, Subclass, arrow, element].map(value => Bun.inspect(value)))); + `, + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "main.mjs"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ inspected: JSON.parse(stdout), stderr, exitCode }).toEqual({ + inspected: ["[class default]", "[class default extends Base]", "[Function: default]", ""], + stderr: "", + exitCode: 0, + }); +}); + 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 1dda628bfa338b01adfa9c0d3bfcc692c684ea74 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 14 Aug 2026 19:29:41 +0000 Subject: [PATCH 2/3] Apply the *default* -> "default" mapping as a post-filter instead of reimplementing getCalculatedDisplayName The previous revision replaced JSC::getCalculatedDisplayName with a local copy of the displayName/name chain that used Bun's stack-trace rule for builtin functions. JSC__JSValue__getName therefore started printing the inferred ecmaName of functions defined in Bun's built-in modules, which are JSC builtins: Bun.inspect(zlib.gzip) became "[Function: fn]" and fs.promises wrappers became "[AsyncFunction: wrapped]". Keep every existing lookup as it was and pass its result through functionNameForDisplay(), which swaps the private *default* identifier's string (compared by StringImpl identity) for "default". Add a test pinning the inspect output of builtin-module functions. --- src/jsc/bindings/ErrorStackTrace.cpp | 142 ++++++++++++++++----------- src/jsc/bindings/ErrorStackTrace.h | 14 +-- src/jsc/bindings/bindings.cpp | 5 +- test/js/bun/util/inspect.test.js | 14 +++ 4 files changed, 106 insertions(+), 69 deletions(-) diff --git a/src/jsc/bindings/ErrorStackTrace.cpp b/src/jsc/bindings/ErrorStackTrace.cpp index 681adddf26a2..6ae14da3626a 100644 --- a/src/jsc/bindings/ErrorStackTrace.cpp +++ b/src/jsc/bindings/ErrorStackTrace.cpp @@ -524,56 +524,15 @@ String sourceURL(JSC::VM& vm, JSC::JSFunction* function) return Zig::sourceURL(function->jsExecutable()->source()); } -String functionNameForDisplay(JSC::VM& vm, const JSC::Identifier& name) +String functionNameForDisplay(JSC::VM& vm, String name) { - if (name == vm.propertyNames->starDefaultPrivateName) { + // Compared by identity: the private identifier's string is its own unique StringImpl, so a + // function that is really called "starDefault" keeps its name. + if (name.impl() == vm.propertyNames->starDefaultPrivateName.impl()) { return vm.propertyNames->defaultKeyword.string(); } - return name.string(); -} - -// Own data properties only: this also runs from error finalizers, where getters must not run -// and nothing may be allocated on the JS heap. -static String ownStringPropertyWithoutGC(JSC::JSObject* object, const JSC::Identifier& propertyName) -{ - unsigned attributes; - PropertyOffset offset = object->structure()->getConcurrently(propertyName.impl(), attributes); - if (offset == invalidOffset || (attributes & (PropertyAttribute::Accessor | PropertyAttribute::CustomAccessorOrValue))) { - return String(); - } - - JSValue value = object->getDirect(offset); - if (!value || !value.isString()) { - return String(); - } - - return asString(value)->tryGetValueWithoutGC(); -} - -String calculatedDisplayName(JSC::VM& vm, JSC::JSObject* object) -{ - auto jstype = object->type(); - if (jstype != JSC::JSFunctionType && jstype != JSC::InternalFunctionType) { - return emptyString(); - } - - String displayName = ownStringPropertyWithoutGC(object, vm.propertyNames->displayName); - if (!displayName.isEmpty()) { - return displayName; - } - - if (jstype == JSC::InternalFunctionType) { - return uncheckedDowncast(object)->name(); - } - - auto* function = uncheckedDowncast(object); - String name = function->nameWithoutGC(vm); - if (!name.isEmpty() || function->isHostFunction()) { - return name; - } - - return functionNameForDisplay(vm, function->jsExecutable()->ecmaName()); + return name; } String functionName(JSC::VM& vm, JSC::CodeBlock* codeBlock) @@ -586,7 +545,7 @@ String functionName(JSC::VM& vm, JSC::CodeBlock* codeBlock) } if (codeType == JSC::FunctionCode) { - return functionNameForDisplay(vm, uncheckedDowncast(executable)->ecmaName()); + return functionNameForDisplay(vm, uncheckedDowncast(executable)->ecmaName().string()); } return String(); @@ -594,7 +553,9 @@ String functionName(JSC::VM& vm, JSC::CodeBlock* codeBlock) String functionName(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSObject* object) { - if (object->type() == JSC::ProxyObjectType) return {}; + WTF::String functionName; + auto jstype = object->type(); + if (jstype == JSC::ProxyObjectType) return {}; // First try the "name" property. { @@ -617,7 +578,30 @@ String functionName(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC:: } } - return calculatedDisplayName(vm, object); + { + // Then try the "displayName" property (what this does internally) + auto topExceptionScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + functionName = JSC::getCalculatedDisplayName(vm, object); + if (topExceptionScope.exception()) [[unlikely]] { + (void)topExceptionScope.tryClearException(); + } + } + + { + if (functionName.isEmpty()) { + if (jstype == JSC::JSFunctionType) { + auto* function = uncheckedDowncast(object); + functionName = function->nameWithoutGC(vm); + if (functionName.isEmpty() && !function->isHostFunction()) { + functionName = function->jsExecutable()->ecmaName().string(); + } + } else if (jstype == JSC::InternalFunctionType) { + functionName = uncheckedDowncast(object)->name(); + } + } + } + + return functionNameForDisplay(vm, WTF::move(functionName)); } String functionName(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, const JSC::StackFrame& frame, FinalizerSafety finalizerSafety, unsigned int* flags) @@ -628,17 +612,63 @@ String functionName(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, const if (auto* callee = frame.callee()) { if (auto* object = callee->getObject()) { auto jstype = object->type(); - if (flags && (jstype == JSC::JSFunctionType || jstype == JSC::InternalFunctionType)) { - *flags |= static_cast(FunctionNameFlags::Function); - } + Structure* structure = object->structure(); + + auto setTypeFlagsIfNecessary = [&]() { + if (flags) { + if (jstype == JSC::JSFunctionType || jstype == JSC::InternalFunctionType) { + *flags |= static_cast(FunctionNameFlags::Function); + } + } + }; // First try the "name" property. - String name = ownStringPropertyWithoutGC(object, vm.propertyNames->name); - if (!name.isEmpty()) { - return name; + { + unsigned attributes; + PropertyOffset offset = structure->getConcurrently(vm.propertyNames->name.impl(), attributes); + if (offset != invalidOffset && !(attributes & (PropertyAttribute::Accessor | PropertyAttribute::CustomAccessorOrValue))) { + JSValue name = object->getDirect(offset); + if (name && name.isString()) { + auto str = asString(name)->tryGetValueWithoutGC(); + if (!str->isEmpty()) { + setTypeFlagsIfNecessary(); + return str; + } + } + } + } + + // Then try the "displayName" property. + { + unsigned attributes; + PropertyOffset offset = structure->getConcurrently(vm.propertyNames->displayName.impl(), attributes); + if (offset != invalidOffset && !(attributes & (PropertyAttribute::Accessor | PropertyAttribute::CustomAccessorOrValue))) { + JSValue name = object->getDirect(offset); + if (name && name.isString()) { + auto str = asString(name)->tryGetValueWithoutGC(); + if (!str->isEmpty()) { + setTypeFlagsIfNecessary(); + return str; + } + } + } } - return calculatedDisplayName(vm, object); + // Lastly, try type-specific properties. + if (jstype == JSC::JSFunctionType) { + auto* function = uncheckedDowncast(object); + auto str = function->nameWithoutGC(vm); + if (str.isEmpty() && !function->isHostFunction()) { + setTypeFlagsIfNecessary(); + return functionNameForDisplay(vm, function->jsExecutable()->ecmaName().string()); + } + setTypeFlagsIfNecessary(); + return str; + } else if (jstype == JSC::InternalFunctionType) { + auto str = uncheckedDowncast(object)->name(); + setTypeFlagsIfNecessary(); + return str; + } } } diff --git a/src/jsc/bindings/ErrorStackTrace.h b/src/jsc/bindings/ErrorStackTrace.h index 8eb1df5f62a4..c10afd7457ad 100644 --- a/src/jsc/bindings/ErrorStackTrace.h +++ b/src/jsc/bindings/ErrorStackTrace.h @@ -235,16 +235,10 @@ class FunctionNameFlags { static constexpr unsigned AddNewKeyword = 1 << 4; }; -// The name JSC recorded for a function, as it should be shown to users. An anonymous -// `export default` is bound to the private `*default*` identifier, which stringifies as -// "starDefault"; its reified `name` property is "default" (JSFunction::reifyName). -String functionNameForDisplay(JSC::VM& vm, const JSC::Identifier& name); - -// Bun's counterpart of JSC::getCalculatedDisplayName: a non-empty own "displayName" property, else -// the function's own name, else (for JS functions) its ecmaName via functionNameForDisplay. Empty -// for anything that is not a function. Runs no JS and allocates nothing on the JS heap, so it is -// also used to format stack traces from error finalizers. -String calculatedDisplayName(JSC::VM& vm, JSC::JSObject* object); +// An anonymous `export default` is bound to JSC's private `*default*` identifier, whose string is +// "starDefault". JSC only substitutes "default" when it reifies the function's `name` property +// (JSFunction::reifyName); apply the same substitution to a name taken from the executable instead. +String functionNameForDisplay(JSC::VM& vm, String name); String functionName(JSC::VM& vm, JSC::CodeBlock* codeBlock); String functionName(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSObject* callee); diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index 15b757f07e7a..708ed59226d8 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -4957,8 +4957,7 @@ void JSC__JSValue__getNameProperty(JSC::EncodedJSValue JSValue0, JSC::JSGlobalOb return; } - // JSFunction::name() returns "" for the `*default*` binding of an anonymous `export default`. - actualName = Zig::functionNameForDisplay(vm, function->jsExecutable()->name()); + actualName = Zig::functionNameForDisplay(vm, function->jsExecutable()->name().string()); *arg2 = Zig::toZigString(actualName); return; @@ -4982,7 +4981,7 @@ 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 = Zig::calculatedDisplayName(vm, object); + auto displayName = Zig::functionNameForDisplay(vm, 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 a7d88210ae4b..58ed181c9531 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -1,5 +1,7 @@ import { describe, expect, it } from "bun:test"; import { bunEnv, bunExe, isASAN, isWindows, normalizeBunSnapshot, tempDir, tmpdirSync } from "harness"; +import { readdir } from "node:fs/promises"; +import { gzip } from "node:zlib"; import { join } from "path"; import util from "util"; it("prototype", () => { @@ -651,6 +653,18 @@ it("anonymous export default class and function are named 'default'", async () = }); }); +// Bun's built-in modules are compiled as JSC builtins. zlib's convenience methods and fs.promises' +// wrappers are anonymous function expressions that get their `name` defined afterwards; the local +// variable each was assigned to inside the module (`fn`, `wrapped`) must not show up here. +it("functions from built-in modules do not inspect as their internal variable name", () => { + expect([gzip.name, Bun.inspect(gzip), readdir.name, Bun.inspect(readdir)]).toEqual([ + "gzip", + "[Function]", + "readdir", + "[AsyncFunction]", + ]); +}); + 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 cfd9586149c79126a16d4782388c33ac341e7ba6 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 14 Aug 2026 19:39:56 +0000 Subject: [PATCH 3/3] Shorten the functionNameForDisplay comments --- src/jsc/bindings/ErrorStackTrace.cpp | 3 +-- src/jsc/bindings/ErrorStackTrace.h | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/jsc/bindings/ErrorStackTrace.cpp b/src/jsc/bindings/ErrorStackTrace.cpp index 6ae14da3626a..e6a76f29f206 100644 --- a/src/jsc/bindings/ErrorStackTrace.cpp +++ b/src/jsc/bindings/ErrorStackTrace.cpp @@ -526,8 +526,7 @@ String sourceURL(JSC::VM& vm, JSC::JSFunction* function) String functionNameForDisplay(JSC::VM& vm, String name) { - // Compared by identity: the private identifier's string is its own unique StringImpl, so a - // function that is really called "starDefault" keeps its name. + // Identity, not equality: a function that is actually named "starDefault" keeps its name. if (name.impl() == vm.propertyNames->starDefaultPrivateName.impl()) { return vm.propertyNames->defaultKeyword.string(); } diff --git a/src/jsc/bindings/ErrorStackTrace.h b/src/jsc/bindings/ErrorStackTrace.h index c10afd7457ad..4048424526e8 100644 --- a/src/jsc/bindings/ErrorStackTrace.h +++ b/src/jsc/bindings/ErrorStackTrace.h @@ -235,9 +235,7 @@ class FunctionNameFlags { static constexpr unsigned AddNewKeyword = 1 << 4; }; -// An anonymous `export default` is bound to JSC's private `*default*` identifier, whose string is -// "starDefault". JSC only substitutes "default" when it reifies the function's `name` property -// (JSFunction::reifyName); apply the same substitution to a name taken from the executable instead. +// Renders an anonymous `export default`'s internal `*default*` binding as "default", like JSFunction::reifyName does for `.name`. String functionNameForDisplay(JSC::VM& vm, String name); String functionName(JSC::VM& vm, JSC::CodeBlock* codeBlock);