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
16 changes: 13 additions & 3 deletions src/jsc/bindings/ErrorStackTrace.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,16 @@ String sourceURL(JSC::VM& vm, JSC::JSFunction* function)
return Zig::sourceURL(function->jsExecutable()->source());
}

String functionNameForDisplay(JSC::VM& vm, String 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();
}

return name;
}

String functionName(JSC::VM& vm, JSC::CodeBlock* codeBlock)
{
auto codeType = codeBlock->codeType();
Expand All @@ -534,7 +544,7 @@ String functionName(JSC::VM& vm, JSC::CodeBlock* codeBlock)
}

if (codeType == JSC::FunctionCode) {
return uncheckedDowncast<JSC::FunctionExecutable>(executable)->ecmaName().string();
return functionNameForDisplay(vm, uncheckedDowncast<JSC::FunctionExecutable>(executable)->ecmaName().string());
}

return String();
Expand Down Expand Up @@ -590,7 +600,7 @@ String functionName(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::
}
}

return functionName;
return functionNameForDisplay(vm, WTF::move(functionName));
}

String functionName(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, const JSC::StackFrame& frame, FinalizerSafety finalizerSafety, unsigned int* flags)
Expand Down Expand Up @@ -649,7 +659,7 @@ String functionName(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, const
auto str = function->nameWithoutGC(vm);
if (str.isEmpty() && !function->isHostFunction()) {
setTypeFlagsIfNecessary();
return function->jsExecutable()->ecmaName().string();
return functionNameForDisplay(vm, function->jsExecutable()->ecmaName().string());
}
setTypeFlagsIfNecessary();
return str;
Expand Down
3 changes: 3 additions & 0 deletions src/jsc/bindings/ErrorStackTrace.h
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,9 @@ class FunctionNameFlags {
static constexpr unsigned AddNewKeyword = 1 << 4;
};

// 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);
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);
Expand Down
4 changes: 2 additions & 2 deletions src/jsc/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4957,7 +4957,7 @@ void JSC__JSValue__getNameProperty(JSC::EncodedJSValue JSValue0, JSC::JSGlobalOb
return;
}

actualName = function->jsExecutable()->name().string();
actualName = Zig::functionNameForDisplay(vm, function->jsExecutable()->name().string());

*arg2 = Zig::toZigString(actualName);
return;
Expand All @@ -4981,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 = JSC::getCalculatedDisplayName(vm, object);
auto displayName = Zig::functionNameForDisplay(vm, JSC::getCalculatedDisplayName(vm, object));

// JSC doesn't include @@toStringTag in calculated display name
if (displayName.isEmpty()) {
Expand Down
121 changes: 119 additions & 2 deletions test/js/bun/test/stack.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -149,3 +149,120 @@ test("Async functions frame should be included in stack trace", async () => {
at async <anonymous> (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 <dir>/uncaught.mjs"]],
["unhandled-rejection.mjs", ["at new default (file:NN:NN)", "at <anonymous> (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 });
});
});
50 changes: 49 additions & 1 deletion test/js/bun/util/inspect.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
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 { readdir } from "node:fs/promises";
import { gzip } from "node:zlib";
import { join } from "path";
import util from "util";
it("prototype", () => {
Expand Down Expand Up @@ -617,6 +619,52 @@ 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]", "<default />"],
stderr: "",
exitCode: 0,
});
});

// 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}');
Expand Down
Loading