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
144 changes: 139 additions & 5 deletions src/jsc/bindings/CallSitePrototype.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
#include <JavaScriptCore/JSCInlines.h>
#include <JavaScriptCore/ObjectConstructor.h>
#include <JavaScriptCore/JSBoundFunction.h>
#include <JavaScriptCore/PropertyNameArray.h>
#include <JavaScriptCore/ProxyObject.h>
using namespace JSC;

namespace Zig {
Expand Down Expand Up @@ -100,11 +102,37 @@ JSC_DEFINE_HOST_FUNCTION(callSiteProtoFuncGetThis, (JSGlobalObject * globalObjec
return JSC::JSValue::encode(callSite->thisValue());
}

// TODO: doesn't get class name
// Matches V8's CallSiteInfo::GetTypeName: returns the name of the constructor
// that produced the receiver (or its prototype's constructor), falling back to
// @@toStringTag / [[Class]]. Returns null for strict-mode frames and for
// null/undefined receivers, never the string "undefined" or an empty string.
JSC_DEFINE_HOST_FUNCTION(callSiteProtoFuncGetTypeName, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
ENTER_PROTO_FUNC();
return JSC::JSValue::encode(JSC::jsTypeStringForValue(globalObject, callSite->thisValue()));

JSC::JSValue thisValue = callSite->thisValue();

// V8 returns null when the receiver can't be identified: strict-mode
// frames, explicit `this === null`/`undefined`, and calls on the global
// object.
if (!thisValue || thisValue.isUndefinedOrNull()) {
return JSC::JSValue::encode(JSC::jsNull());
}

JSC::JSObject* thisObject = thisValue.toObject(globalObject);
RETURN_IF_EXCEPTION(scope, {});
if (!thisObject || thisObject->isGlobalObject()) {
return JSC::JSValue::encode(JSC::jsNull());
}

// JSObject::calculatedClassName implements the V8 cascade: own
// constructor -> proto's constructor -> @@toStringTag -> classInfo -> "Object".
String className = JSObject::calculatedClassName(thisObject);
if (className.isEmpty()) {
return JSC::JSValue::encode(JSC::jsNull());
}

return JSC::JSValue::encode(JSC::jsString(vm, className));
}

JSC_DEFINE_HOST_FUNCTION(callSiteProtoFuncGetFunction, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
Expand All @@ -113,16 +141,122 @@ JSC_DEFINE_HOST_FUNCTION(callSiteProtoFuncGetFunction, (JSGlobalObject * globalO
return JSC::JSValue::encode(callSite->function());
}

// V8 returns null (not "") when the frame's function has no name.
JSC_DEFINE_HOST_FUNCTION(callSiteProtoFuncGetFunctionName, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
ENTER_PROTO_FUNC();
return JSC::JSValue::encode(callSite->functionName());
JSValue name = callSite->functionName();
if (JSC::JSString* str = dynamicDowncast<JSC::JSString>(name)) {
if (str->length() == 0) {
return JSC::JSValue::encode(JSC::jsNull());
}
return JSC::JSValue::encode(str);
}
return JSC::JSValue::encode(JSC::jsNull());
}

// TODO
// Matches V8's CallSiteInfo::GetMethodName: walks the receiver and its
// prototype chain looking for a property whose value is identity-equal to the
// frame's function. Returns null if the receiver is nullish, if no matching
// property is found, or if more than one distinct property name matches.
JSC_DEFINE_HOST_FUNCTION(callSiteProtoFuncGetMethodName, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
return callSiteProtoFuncGetFunctionName(globalObject, callFrame);
ENTER_PROTO_FUNC();

JSC::JSValue thisValue = callSite->thisValue();
if (!thisValue || thisValue.isUndefinedOrNull()) {
return JSC::JSValue::encode(JSC::jsNull());
}

JSC::JSValue functionValue = callSite->function();
if (!functionValue || !functionValue.isCell()) {
return JSC::JSValue::encode(JSC::jsNull());
}
JSC::JSCell* functionCell = functionValue.asCell();

JSC::JSObject* thisObject = thisValue.toObject(globalObject);
RETURN_IF_EXCEPTION(scope, {});
if (!thisObject) {
return JSC::JSValue::encode(JSC::jsNull());
}

// Walk `this` and its prototype chain. Collect property names whose value
// is identity-equal to the frame's function. Bound functions and proxies
// aren't inspected (V8 gives up on them too).
Identifier foundName;
bool foundMultiple = false;

JSC::JSObject* current = thisObject;
// Cap the depth to avoid pathological prototype chains; 128 matches other
// JSC traversals and is more than any real chain.
constexpr unsigned maxDepth = 128;
for (unsigned depth = 0; current && depth < maxDepth; ++depth) {
if (current->type() == JSC::ProxyObjectType) {
break;
}

PropertyNameArrayBuilder properties(vm, PropertyNameMode::Strings, PrivateSymbolMode::Exclude);
{
auto topExceptionScope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
current->methodTable()->getOwnPropertyNames(current, globalObject, properties, DontEnumPropertiesMode::Include);
if (topExceptionScope.exception()) [[unlikely]] {
(void)topExceptionScope.tryClearException();
break;
}
}

for (const auto& propertyName : properties) {
PropertySlot slot(current, PropertySlot::InternalMethodType::VMInquiry, &vm);
auto topExceptionScope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
bool hasProperty = current->methodTable()->getOwnPropertySlot(current, globalObject, propertyName, slot);
if (topExceptionScope.exception()) [[unlikely]] {
(void)topExceptionScope.tryClearException();
continue;
}
if (!hasProperty) {
continue;
}
// Only examine direct values (mirrors V8: accessor/computed props
// would require invoking user code).
if (!slot.isValue()) {
continue;
}
JSValue value = slot.getValue(globalObject, propertyName);
if (topExceptionScope.exception()) [[unlikely]] {
(void)topExceptionScope.tryClearException();
continue;
}
if (!value.isCell() || value.asCell() != functionCell) {
continue;
}

if (foundName.isNull()) {
foundName = propertyName;
} else if (foundName != propertyName) {
foundMultiple = true;
break;
}
}

if (foundMultiple) {
break;
}

if (current->structure()->typeInfo().overridesGetPrototype()) [[unlikely]] {
break;
}
JSValue protoValue = current->getPrototypeDirect();
if (!protoValue.isObject()) {
break;
}
current = asObject(protoValue);
}

if (foundName.isNull() || foundMultiple) {
return JSC::JSValue::encode(JSC::jsNull());
}

return JSC::JSValue::encode(JSC::jsString(vm, foundName.string()));
}

JSC_DEFINE_HOST_FUNCTION(callSiteProtoFuncGetFileName, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
Expand Down
57 changes: 53 additions & 4 deletions test/js/node/v8/capture-stack-trace.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -350,11 +350,14 @@ test("sanity check", () => {
Error.prepareStackTrace = (e, s) => {
// getThis returns undefined in strict mode
expect(s[0].getThis()).toBe(undefined);
expect(s[0].getTypeName()).toBe("undefined");
// getTypeName returns null when the receiver is null/undefined or strict.
// Matches V8 (previously Bun returned the literal string "undefined").
expect(s[0].getTypeName()).toBe(null);
// getFunction returns undefined in strict mode
expect(s[0].getFunction()).toBe(undefined);
expect(s[0].getFunctionName()).toBe("f3");
expect(s[0].getMethodName()).toBe("f3");
// f3 is a top-level function, not a method of any object, so V8 returns null.
expect(s[0].getMethodName()).toBe(null);
expect(typeof s[0].getLineNumber()).toBe("number");
expect(typeof s[0].getColumnNumber()).toBe("number");
expect(s[0].getFileName().includes("capture-stack-trace.test.js")).toBe(true);
Expand Down Expand Up @@ -465,6 +468,52 @@ test("CallFrame.p.isConstructor", () => {
Error.prepareStackTrace = prevPrepareStackTrace;
});

// Regression for https://github.com/oven-sh/bun/issues/30938
// getTypeName/getFunctionName/getMethodName must match V8 (return null,
// not the string "undefined" or the empty string). tap + source-map-support
// concatenates `typeName + "."` into the formatted frame, so the old
// behaviour rendered as `undefined.<anonymous>` in TAP error output.
test("CallSite.p.getTypeName returns null (not 'undefined') when receiver is not an object", () => {
let prevPrepareStackTrace = Error.prepareStackTrace;
Error.prepareStackTrace = (_, s) => {
expect(s[0].getTypeName()).toBe(null);
};
const e = new Error();
Error.captureStackTrace(e);
e.stack;
Error.prepareStackTrace = prevPrepareStackTrace;
});

test("CallSite.p.getFunctionName returns null for anonymous frames", () => {
let prevPrepareStackTrace = Error.prepareStackTrace;
Error.prepareStackTrace = (_, s) => {
// Top frame is the anonymous arrow below.
expect(s[0].getFunctionName()).toBe(null);
};
(() => {
const e = new Error();
Error.captureStackTrace(e);
e.stack;
})();
Error.prepareStackTrace = prevPrepareStackTrace;
});

test("CallSite.p.getMethodName returns null for non-method frames", () => {
let prevPrepareStackTrace = Error.prepareStackTrace;
function topLevelFn() {
const e = new Error();
Error.captureStackTrace(e);
e.stack;
}
Error.prepareStackTrace = (_, s) => {
// topLevelFn is a plain function call — it's not stored at a property of
// `this`, so V8 returns null. Previously Bun delegated to getFunctionName.
expect(s[0].getMethodName()).toBe(null);
};
topLevelFn();
Error.prepareStackTrace = prevPrepareStackTrace;
});

test("CallFrame.p.isNative", () => {
let prevPrepareStackTrace = Error.prepareStackTrace;
Error.prepareStackTrace = (e, s) => {
Expand Down Expand Up @@ -523,8 +572,8 @@ test("err.stack should invoke prepareStackTrace", () => {
functionWithAName();

expect(functionName).toBe("functionWithAName");
expect(lineNumber).toBe(518);
expect(parentLineNumber).toBe(523);
expect(lineNumber).toBe(567);
expect(parentLineNumber).toBe(572);
});

test("Error.prepareStackTrace inside a node:vm works", () => {
Expand Down
Loading