diff options
author | 2023-09-20 05:52:59 -0700 | |
---|---|---|
committer | 2023-09-20 05:52:59 -0700 | |
commit | ff7f642099b2ce777347b125e802083d324d3cbd (patch) | |
tree | fd10a5207a64dd13ee6b02855a5c47c05ab597f5 | |
parent | 1456c72648927e54ab78b00205917258672a4ecc (diff) | |
download | bun-ff7f642099b2ce777347b125e802083d324d3cbd.tar.gz bun-ff7f642099b2ce777347b125e802083d324d3cbd.tar.zst bun-ff7f642099b2ce777347b125e802083d324d3cbd.zip |
Call `Error.prepareStackTrace` on `new Error().stack` (#5802)
* Always call `Error.prepareStackTrace`
* Support node:vm
* Remove this
* Handle more cases
---------
Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
-rw-r--r-- | src/bun.js/bindings/ErrorStackTrace.cpp | 76 | ||||
-rw-r--r-- | src/bun.js/bindings/ErrorStackTrace.h | 12 | ||||
-rw-r--r-- | src/bun.js/bindings/ZigGlobalObject.cpp | 267 | ||||
-rw-r--r-- | src/bun.js/bindings/ZigGlobalObject.h | 6 | ||||
-rw-r--r-- | test/js/node/v8/capture-stack-trace.test.js | 69 | ||||
-rw-r--r-- | test/js/node/vm/vm.test.ts | 2 |
6 files changed, 277 insertions, 155 deletions
diff --git a/src/bun.js/bindings/ErrorStackTrace.cpp b/src/bun.js/bindings/ErrorStackTrace.cpp index be20f7737..f7013c9c4 100644 --- a/src/bun.js/bindings/ErrorStackTrace.cpp +++ b/src/bun.js/bindings/ErrorStackTrace.cpp @@ -131,8 +131,8 @@ JSCStackFrame::JSCStackFrame(JSC::VM& vm, JSC::StackVisitor& visitor) : m_vm(vm) , m_codeBlock(nullptr) , m_bytecodeIndex(JSC::BytecodeIndex()) - , m_sourceURL(nullptr) - , m_functionName(nullptr) + , m_sourceURL() + , m_functionName() , m_isWasmFrame(false) , m_sourcePositionsState(SourcePositionsState::NotCalculated) { @@ -163,8 +163,8 @@ JSCStackFrame::JSCStackFrame(JSC::VM& vm, const JSC::StackFrame& frame) , m_callFrame(nullptr) , m_codeBlock(nullptr) , m_bytecodeIndex(JSC::BytecodeIndex()) - , m_sourceURL(nullptr) - , m_functionName(nullptr) + , m_sourceURL() + , m_functionName() , m_isWasmFrame(false) , m_sourcePositionsState(SourcePositionsState::NotCalculated) { @@ -193,7 +193,7 @@ JSC::JSString* JSCStackFrame::sourceURL() m_sourceURL = retrieveSourceURL(); } - return m_sourceURL; + return jsString(this->m_vm, m_sourceURL); } JSC::JSString* JSCStackFrame::functionName() @@ -202,7 +202,7 @@ JSC::JSString* JSCStackFrame::functionName() m_functionName = retrieveFunctionName(); } - return m_functionName; + return jsString(this->m_vm, m_functionName); } JSC::JSString* JSCStackFrame::typeName() @@ -211,7 +211,7 @@ JSC::JSString* JSCStackFrame::typeName() m_typeName = retrieveTypeName(); } - return m_typeName; + return jsString(this->m_vm, m_typeName); } JSCStackFrame::SourcePositions* JSCStackFrame::getSourcePositions() @@ -223,91 +223,61 @@ JSCStackFrame::SourcePositions* JSCStackFrame::getSourcePositions() return (SourcePositionsState::Calculated == m_sourcePositionsState) ? &m_sourcePositions : nullptr; } -ALWAYS_INLINE JSC::JSString* JSCStackFrame::retrieveSourceURL() +ALWAYS_INLINE String JSCStackFrame::retrieveSourceURL() { static auto sourceURLWasmString = MAKE_STATIC_STRING_IMPL("[wasm code]"); static auto sourceURLNativeString = MAKE_STATIC_STRING_IMPL("[native code]"); if (m_isWasmFrame) { - return jsOwnedString(m_vm, sourceURLWasmString); + return String(sourceURLWasmString); } if (!m_codeBlock) { - return jsOwnedString(m_vm, sourceURLNativeString); + return String(sourceURLNativeString); } - String sourceURL = m_codeBlock->ownerExecutable()->sourceURL(); - return sourceURL.isNull() ? m_vm.smallStrings.emptyString() : JSC::jsString(m_vm, sourceURL); + return m_codeBlock->ownerExecutable()->sourceURL(); } -ALWAYS_INLINE JSC::JSString* JSCStackFrame::retrieveFunctionName() +ALWAYS_INLINE String JSCStackFrame::retrieveFunctionName() { static auto functionNameEvalCodeString = MAKE_STATIC_STRING_IMPL("eval code"); static auto functionNameModuleCodeString = MAKE_STATIC_STRING_IMPL("module code"); static auto functionNameGlobalCodeString = MAKE_STATIC_STRING_IMPL("global code"); if (m_isWasmFrame) { - return jsString(m_vm, JSC::Wasm::makeString(m_wasmFunctionIndexOrName)); + return JSC::Wasm::makeString(m_wasmFunctionIndexOrName); } if (m_codeBlock) { switch (m_codeBlock->codeType()) { case JSC::EvalCode: - return JSC::jsOwnedString(m_vm, functionNameEvalCodeString); + return String(functionNameEvalCodeString); case JSC::ModuleCode: - return JSC::jsOwnedString(m_vm, functionNameModuleCodeString); + return String(functionNameModuleCodeString); case JSC::FunctionCode: break; case JSC::GlobalCode: - return JSC::jsOwnedString(m_vm, functionNameGlobalCodeString); + return String(functionNameGlobalCodeString); default: ASSERT_NOT_REACHED(); } } - if (!m_callee || !m_callee->isObject()) { - return m_vm.smallStrings.emptyString(); + String name; + if (m_callee) { + if (m_callee->isObject()) + name = getCalculatedDisplayName(m_vm, jsCast<JSObject*>(m_callee)).impl(); } - JSC::JSObject* calleeAsObject = JSC::jsCast<JSC::JSObject*>(m_callee); - - // First, try the "displayName" property - JSC::JSValue displayName = calleeAsObject->getDirect(m_vm, m_vm.propertyNames->displayName); - if (displayName && isJSString(displayName)) { - return JSC::asString(displayName); - } - - // Our addition - if there's no "dispalyName" property, try the "name" property - JSC::JSValue name = calleeAsObject->getDirect(m_vm, m_vm.propertyNames->name); - if (name && isJSString(name)) { - return JSC::asString(name); - } - - /* For functions (either JSFunction or InternalFunction), fallback to their "native" name property. - * Based on JSC::getCalculatedDisplayName, "inlining" the - * JSFunction::calculatedDisplayName\InternalFunction::calculatedDisplayName calls */ - if (JSC::JSFunction* function = JSC::jsDynamicCast<JSC::JSFunction*>(calleeAsObject)) { - // Based on JSC::JSFunction::calculatedDisplayName, skipping the "displayName" property check - WTF::String actualName = function->name(m_vm); - if (!actualName.isEmpty() || function->isHostOrBuiltinFunction()) { - return JSC::jsString(m_vm, actualName); - } - - return JSC::jsString(m_vm, function->jsExecutable()->name().string()); - } - if (JSC::InternalFunction* function = JSC::jsDynamicCast<JSC::InternalFunction*>(calleeAsObject)) { - // Based on JSC::InternalFunction::calculatedDisplayName, skipping the "displayName" property check - return JSC::jsString(m_vm, function->name()); - } - - return m_vm.smallStrings.emptyString(); + return name.isNull() ? emptyString() : name; } -ALWAYS_INLINE JSC::JSString* JSCStackFrame::retrieveTypeName() +ALWAYS_INLINE String JSCStackFrame::retrieveTypeName() { JSC::JSObject* calleeObject = JSC::jsCast<JSC::JSObject*>(m_callee); // return JSC::jsTypeStringForValue(m_globalObjectcalleeObject->toThis() - return jsString(m_vm, makeString(calleeObject->className())); + return calleeObject->className(); } // General flow here is based on JSC's appendSourceToError (ErrorInstance.cpp) diff --git a/src/bun.js/bindings/ErrorStackTrace.h b/src/bun.js/bindings/ErrorStackTrace.h index 1284376a4..ac40eaf4b 100644 --- a/src/bun.js/bindings/ErrorStackTrace.h +++ b/src/bun.js/bindings/ErrorStackTrace.h @@ -61,9 +61,9 @@ private: JSC::BytecodeIndex m_bytecodeIndex; // Lazy-initialized - JSC::JSString* m_sourceURL; - JSC::JSString* m_functionName; - JSC::JSString* m_typeName; + WTF::String m_sourceURL; + WTF::String m_functionName; + WTF::String m_typeName; // m_wasmFunctionIndexOrName has meaning only when m_isWasmFrame is set JSC::Wasm::IndexOrName m_wasmFunctionIndexOrName; @@ -105,7 +105,7 @@ public: bool isConstructor() const { return m_codeBlock && (JSC::CodeForConstruct == m_codeBlock->specializationKind()); } private: - ALWAYS_INLINE JSC::JSString* retrieveSourceURL(); + ALWAYS_INLINE String retrieveSourceURL(); /* Regarding real functions (not eval\module\global code), both v8 and JSC seem to follow * the same logic, which is to first try the function's "display name", and if it's not defined, @@ -119,9 +119,9 @@ private: * and just try to use the "name" property when needed, so our lookup will be: * "display name" property -> "name" property -> JSFunction\InternalFunction "name" methods. */ - ALWAYS_INLINE JSC::JSString* retrieveFunctionName(); + ALWAYS_INLINE String retrieveFunctionName(); - ALWAYS_INLINE JSC::JSString* retrieveTypeName(); + ALWAYS_INLINE String retrieveTypeName(); bool calculateSourcePositions(); }; diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 9b36157fe..2f3aa2209 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -315,29 +315,16 @@ static bool skipNextComputeErrorInfo = false; // error.stack calls this function static String computeErrorInfoWithoutPrepareStackTrace(JSC::VM& vm, Vector<StackFrame>& stackTrace, unsigned& line, unsigned& column, String& sourceURL, JSObject* errorInstance) { - if (!errorInstance) { - return String(); - } - - if (skipNextComputeErrorInfo) { - return String(); - } - - Zig::GlobalObject* globalObject = jsDynamicCast<Zig::GlobalObject*>(errorInstance->globalObject()); - if (!globalObject) { - // Happens in node:vm - globalObject = jsDynamicCast<Zig::GlobalObject*>(Bun__getDefaultGlobal()); - } + auto* lexicalGlobalObject = errorInstance->globalObject(); + Zig::GlobalObject* globalObject = jsDynamicCast<Zig::GlobalObject*>(lexicalGlobalObject); WTF::String name = "Error"_s; WTF::String message; - if (errorInstance) { - // Note that we are not allowed to allocate memory in here. It's called inside a finalizer. - if (auto* instance = jsDynamicCast<ErrorInstance*>(errorInstance)) { - name = instance->sanitizedNameString(globalObject); - message = instance->sanitizedMessageString(globalObject); - } + // Note that we are not allowed to allocate memory in here. It's called inside a finalizer. + if (auto* instance = jsDynamicCast<ErrorInstance*>(errorInstance)) { + name = instance->sanitizedNameString(lexicalGlobalObject); + message = instance->sanitizedMessageString(lexicalGlobalObject); } WTF::StringBuilder sb; @@ -391,20 +378,23 @@ static String computeErrorInfoWithoutPrepareStackTrace(JSC::VM& vm, Vector<Stack unsigned int thisColumn = 0; frame.computeLineAndColumn(thisLine, thisColumn); memset(remappedFrames + i, 0, sizeof(ZigStackFrame)); - remappedFrames[i].position.line = thisLine; remappedFrames[i].position.column_start = thisColumn; + String sourceURLForFrame = frame.sourceURL(vm); - if (!sourceURLForFrame.isEmpty()) { - remappedFrames[i].source_url = Bun::toString(sourceURLForFrame); - } else { - // https://github.com/oven-sh/bun/issues/3595 - remappedFrames[i].source_url = BunStringEmpty; - } + // If it's not a Zig::GlobalObject, don't bother source-mapping it. + if (globalObject) { + if (!sourceURLForFrame.isEmpty()) { + remappedFrames[i].source_url = Bun::toString(sourceURLForFrame); + } else { + // https://github.com/oven-sh/bun/issues/3595 + remappedFrames[i].source_url = BunStringEmpty; + } - // This ensures the lifetime of the sourceURL is accounted for correctly - Bun__remapStackFramePositions(globalObject, remappedFrames + i, 1); + // This ensures the lifetime of the sourceURL is accounted for correctly + Bun__remapStackFramePositions(globalObject, remappedFrames + i, 1); + } if (!hasSet) { hasSet = true; @@ -412,11 +402,9 @@ static String computeErrorInfoWithoutPrepareStackTrace(JSC::VM& vm, Vector<Stack column = thisColumn; sourceURL = frame.sourceURL(vm); - if (errorInstance) { - if (remappedFrames[i].remapped) { - errorInstance->putDirect(vm, Identifier::fromString(vm, "originalLine"_s), jsNumber(thisLine), 0); - errorInstance->putDirect(vm, Identifier::fromString(vm, "originalColumn"_s), jsNumber(thisColumn), 0); - } + if (remappedFrames[i].remapped) { + errorInstance->putDirect(vm, Identifier::fromString(vm, "originalLine"_s), jsNumber(thisLine), 0); + errorInstance->putDirect(vm, Identifier::fromString(vm, "originalColumn"_s), jsNumber(thisColumn), 0); } } @@ -438,8 +426,95 @@ static String computeErrorInfoWithoutPrepareStackTrace(JSC::VM& vm, Vector<Stack return sb.toString(); } +static String computeErrorInfoWithPrepareStackTrace(JSC::VM& vm, Zig::GlobalObject* globalObject, JSC::JSGlobalObject* lexicalGlobalObject, Vector<StackFrame>& stackFrames, unsigned& line, unsigned& column, String& sourceURL, JSObject* errorObject, JSObject* prepareStackTrace) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + size_t stackTraceLimit = globalObject->stackTraceLimit().value(); + if (stackTraceLimit == 0) { + stackTraceLimit = DEFAULT_ERROR_STACK_TRACE_LIMIT; + } + + JSCStackTrace stackTrace = JSCStackTrace::fromExisting(vm, stackFrames); + + // Note: we cannot use tryCreateUninitializedRestricted here because we cannot allocate memory inside initializeIndex() + JSC::JSArray* callSites = JSC::JSArray::create(vm, + globalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous), + stackTrace.size()); + + // Create the call sites (one per frame) + GlobalObject::createCallSitesFromFrames(globalObject, lexicalGlobalObject, stackTrace, callSites); + + // We need to sourcemap it if it's a GlobalObject. + if (globalObject == lexicalGlobalObject) { + size_t framesCount = stackTrace.size(); + ZigStackFrame remappedFrames[framesCount]; + for (int i = 0; i < framesCount; i++) { + memset(remappedFrames + i, 0, sizeof(ZigStackFrame)); + remappedFrames[i].source_url = Bun::toString(lexicalGlobalObject, stackTrace.at(i).sourceURL()); + if (JSCStackFrame::SourcePositions* sourcePositions = stackTrace.at(i).getSourcePositions()) { + remappedFrames[i].position.line = sourcePositions->line.zeroBasedInt(); + remappedFrames[i].position.column_start = sourcePositions->startColumn.zeroBasedInt() + 1; + } else { + remappedFrames[i].position.line = -1; + remappedFrames[i].position.column_start = -1; + } + } + + Bun__remapStackFramePositions(globalObject, remappedFrames, framesCount); + + for (size_t i = 0; i < framesCount; i++) { + JSC::JSValue callSiteValue = callSites->getIndex(lexicalGlobalObject, i); + CallSite* callSite = JSC::jsDynamicCast<CallSite*>(callSiteValue); + if (remappedFrames[i].remapped) { + int32_t remappedColumnStart = remappedFrames[i].position.column_start; + JSC::JSValue columnNumber = JSC::jsNumber(remappedColumnStart); + callSite->setColumnNumber(columnNumber); + + int32_t remappedLine = remappedFrames[i].position.line; + JSC::JSValue lineNumber = JSC::jsNumber(remappedLine); + callSite->setLineNumber(lineNumber); + } + } + } + + globalObject->formatStackTrace(vm, lexicalGlobalObject, errorObject, callSites, prepareStackTrace); + + RETURN_IF_EXCEPTION(scope, String()); + return String(); +} + static String computeErrorInfo(JSC::VM& vm, Vector<StackFrame>& stackTrace, unsigned& line, unsigned& column, String& sourceURL, JSObject* errorInstance) { + if (skipNextComputeErrorInfo) { + return String(); + } + + if (!errorInstance) { + return String(); + } + + auto* lexicalGlobalObject = errorInstance->globalObject(); + Zig::GlobalObject* globalObject = jsDynamicCast<Zig::GlobalObject*>(lexicalGlobalObject); + + // Error.prepareStackTrace - https://v8.dev/docs/stack-trace-api#customizing-stack-traces + if (!globalObject) { + // node:vm will use a different JSGlobalObject + globalObject = Bun__getDefaultGlobal(); + + auto* errorConstructor = lexicalGlobalObject->m_errorStructure.constructor(lexicalGlobalObject); + if (JSValue prepareStackTrace = errorConstructor->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "prepareStackTrace"_s))) { + if (prepareStackTrace.isCell() && prepareStackTrace.isObject() && prepareStackTrace.isCallable()) { + return computeErrorInfoWithPrepareStackTrace(vm, globalObject, lexicalGlobalObject, stackTrace, line, column, sourceURL, errorInstance, prepareStackTrace.getObject()); + } + } + } else { + if (JSValue prepareStackTrace = globalObject->m_errorConstructorPrepareStackTraceValue.get()) { + if (prepareStackTrace.isCell() && prepareStackTrace.isObject() && prepareStackTrace.isCallable()) { + return computeErrorInfoWithPrepareStackTrace(vm, globalObject, lexicalGlobalObject, stackTrace, line, column, sourceURL, errorInstance, prepareStackTrace.getObject()); + } + } + } + return computeErrorInfoWithoutPrepareStackTrace(vm, stackTrace, line, column, sourceURL, errorInstance); } @@ -806,6 +881,28 @@ void GlobalObject::setConsole(void* console) this->setConsoleClient(new Zig::ConsoleClient(console)); } +JSC_DEFINE_CUSTOM_GETTER(errorConstructorPrepareStackTraceGetter, + (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, + JSC::PropertyName)) +{ + Zig::GlobalObject* thisObject = JSC::jsCast<Zig::GlobalObject*>(lexicalGlobalObject); + JSValue value = jsUndefined(); + if (thisObject->m_errorConstructorPrepareStackTraceValue) { + value = thisObject->m_errorConstructorPrepareStackTraceValue.get(); + } + return JSValue::encode(value); +} + +JSC_DEFINE_CUSTOM_SETTER(errorConstructorPrepareStackTraceSetter, + (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, + JSC::EncodedJSValue encodedValue, JSC::PropertyName property)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + Zig::GlobalObject* thisObject = JSC::jsCast<Zig::GlobalObject*>(lexicalGlobalObject); + thisObject->m_errorConstructorPrepareStackTraceValue.set(vm, thisObject, JSValue::decode(encodedValue)); + return true; +} + #pragma mark - Globals JSC_DEFINE_CUSTOM_GETTER(globalOnMessage, @@ -2441,7 +2538,7 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionPerformMicrotaskVariadic, (JSGlobalObject * g return JSValue::encode(jsUndefined()); } -void GlobalObject::createCallSitesFromFrames(JSC::JSGlobalObject* lexicalGlobalObject, JSCStackTrace& stackTrace, JSC::JSArray* callSites) +void GlobalObject::createCallSitesFromFrames(Zig::GlobalObject* globalObject, JSC::JSGlobalObject* lexicalGlobalObject, JSCStackTrace& stackTrace, JSC::JSArray* callSites) { /* From v8's "Stack Trace API" (https://github.com/v8/v8/wiki/Stack-Trace-API): * "To maintain restrictions imposed on strict mode functions, frames that have a @@ -2449,10 +2546,11 @@ void GlobalObject::createCallSitesFromFrames(JSC::JSGlobalObject* lexicalGlobalO * their receiver and function objects. For those frames, getFunction() and getThis() * will return undefined."." */ bool encounteredStrictFrame = false; - GlobalObject* globalObject = reinterpret_cast<GlobalObject*>(lexicalGlobalObject); + // TODO: is it safe to use CallSite structure from a different JSGlobalObject? This case would happen within a node:vm JSC::Structure* callSiteStructure = globalObject->callSiteStructure(); size_t framesCount = stackTrace.size(); + for (size_t i = 0; i < framesCount; i++) { CallSite* callSite = CallSite::create(lexicalGlobalObject, callSiteStructure, stackTrace.at(i), encounteredStrictFrame); callSites->putDirectIndex(lexicalGlobalObject, i, callSite); @@ -2463,40 +2561,19 @@ void GlobalObject::createCallSitesFromFrames(JSC::JSGlobalObject* lexicalGlobalO } } -JSC::JSValue GlobalObject::formatStackTrace(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSObject* errorObject, JSC::JSArray* callSites) +void GlobalObject::formatStackTrace(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSObject* errorObject, JSC::JSArray* callSites, JSValue prepareStackTrace) { auto scope = DECLARE_THROW_SCOPE(vm); - JSValue errorValue = this->get(this, JSC::Identifier::fromString(vm, "Error"_s)); - if (UNLIKELY(scope.exception())) { - return JSValue(); - } - - if (!errorValue || errorValue.isUndefined() || !errorValue.isObject()) { - return JSValue(jsEmptyString(vm)); - } - - auto* errorConstructor = jsDynamicCast<JSC::JSObject*>(errorValue); - - /* If the user has set a callable Error.prepareStackTrace - use it to format the stack trace. */ - JSC::JSValue prepareStackTrace = errorConstructor->getIfPropertyExists(lexicalGlobalObject, JSC::Identifier::fromString(vm, "prepareStackTrace"_s)); - if (prepareStackTrace && prepareStackTrace.isCallable()) { - JSC::CallData prepareStackTraceCallData = JSC::getCallData(prepareStackTrace); - if (prepareStackTraceCallData.type != JSC::CallData::Type::None) { - JSC::MarkedArgumentBuffer arguments; - arguments.append(errorObject); - arguments.append(callSites); - ASSERT(!arguments.hasOverflowed()); + auto* errorConstructor = lexicalGlobalObject->m_errorStructure.constructor(this); - JSC::JSValue result = profiledCall( - lexicalGlobalObject, - JSC::ProfilingReason::Other, - prepareStackTrace, - prepareStackTraceCallData, - errorConstructor, - arguments); - RETURN_IF_EXCEPTION(scope, JSC::jsUndefined()); - return result; + if (!prepareStackTrace) { + if (lexicalGlobalObject->inherits<Zig::GlobalObject>()) { + if (auto prepare = this->m_errorConstructorPrepareStackTraceValue.get()) { + prepareStackTrace = prepare; + } + } else { + prepareStackTrace = errorConstructor->getIfPropertyExists(lexicalGlobalObject, JSC::Identifier::fromString(vm, "prepareStackTrace"_s)); } } @@ -2525,7 +2602,43 @@ JSC::JSValue GlobalObject::formatStackTrace(JSC::VM& vm, JSC::JSGlobalObject* le } } - return JSC::JSValue(jsString(vm, sb.toString())); + bool orignialSkipNextComputeErrorInfo = skipNextComputeErrorInfo; + skipNextComputeErrorInfo = true; + if (errorObject->hasProperty(lexicalGlobalObject, vm.propertyNames->stack)) { + skipNextComputeErrorInfo = true; + errorObject->deleteProperty(lexicalGlobalObject, vm.propertyNames->stack); + } + skipNextComputeErrorInfo = orignialSkipNextComputeErrorInfo; + + // In Node, if you console.log(error.stack) inside Error.prepareStackTrace + // it will display the stack as a formatted string, so we have to do the same. + errorObject->putDirect(vm, vm.propertyNames->stack, JSC::JSValue(jsString(vm, sb.toString())), 0); + + if (prepareStackTrace && prepareStackTrace.isCallable()) { + JSC::CallData prepareStackTraceCallData = JSC::getCallData(prepareStackTrace); + + if (prepareStackTraceCallData.type != JSC::CallData::Type::None) { + JSC::MarkedArgumentBuffer arguments; + arguments.append(errorObject); + arguments.append(callSites); + + JSC::JSValue result = profiledCall( + lexicalGlobalObject, + JSC::ProfilingReason::Other, + prepareStackTrace, + prepareStackTraceCallData, + errorConstructor, + arguments); + + RETURN_IF_EXCEPTION(scope, void()); + + if (result.isUndefinedOrNull()) { + result = jsUndefined(); + } + + errorObject->putDirect(vm, vm.propertyNames->stack, result, 0); + } + } } JSC_DEFINE_HOST_FUNCTION(errorConstructorFuncAppendStackTrace, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) @@ -2581,7 +2694,7 @@ JSC_DEFINE_HOST_FUNCTION(errorConstructorFuncCaptureStackTrace, (JSC::JSGlobalOb stackTrace.size()); // Create the call sites (one per frame) - GlobalObject::createCallSitesFromFrames(lexicalGlobalObject, stackTrace, callSites); + GlobalObject::createCallSitesFromFrames(globalObject, lexicalGlobalObject, stackTrace, callSites); /* Format the stack trace. * Note that v8 won't actually format the stack trace here, but will create a "stack" accessor @@ -2623,23 +2736,9 @@ JSC_DEFINE_HOST_FUNCTION(errorConstructorFuncCaptureStackTrace, (JSC::JSGlobalOb } } - JSC::JSValue formattedStackTrace = globalObject->formatStackTrace(vm, lexicalGlobalObject, errorObject, callSites); + globalObject->formatStackTrace(vm, lexicalGlobalObject, errorObject, callSites, JSC::JSValue()); RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode({})); - bool orignialSkipNextComputeErrorInfo = skipNextComputeErrorInfo; - skipNextComputeErrorInfo = true; - if (errorObject->hasProperty(lexicalGlobalObject, vm.propertyNames->stack)) { - skipNextComputeErrorInfo = true; - errorObject->deleteProperty(lexicalGlobalObject, vm.propertyNames->stack); - } - skipNextComputeErrorInfo = orignialSkipNextComputeErrorInfo; - - if (formattedStackTrace.isUndefinedOrNull()) { - formattedStackTrace = JSC::jsUndefined(); - } - - errorObject->putDirect(vm, vm.propertyNames->stack, formattedStackTrace, 0); - if (auto* instance = jsDynamicCast<JSC::ErrorInstance*>(errorObject)) { // we make a separate copy of the StackTrace unfortunately so that we // can later console.log it without losing the info @@ -3572,11 +3671,12 @@ void GlobalObject::addBuiltinGlobals(JSC::VM& vm) JSC::JSObject* errorConstructor = this->errorConstructor(); errorConstructor->putDirectNativeFunction(vm, this, JSC::Identifier::fromString(vm, "captureStackTrace"_s), 2, errorConstructorFuncCaptureStackTrace, ImplementationVisibility::Public, JSC::NoIntrinsic, PropertyAttribute::DontEnum | 0); errorConstructor->putDirectNativeFunction(vm, this, JSC::Identifier::fromString(vm, "appendStackTrace"_s), 2, errorConstructorFuncAppendStackTrace, ImplementationVisibility::Private, JSC::NoIntrinsic, PropertyAttribute::DontEnum | 0); + errorConstructor->putDirectCustomAccessor(vm, JSC::Identifier::fromString(vm, "prepareStackTrace"_s), JSC::CustomGetterSetter::create(vm, errorConstructorPrepareStackTraceGetter, errorConstructorPrepareStackTraceSetter), PropertyAttribute::DontEnum | 0); JSC::JSObject* consoleObject = this->get(this, JSC::Identifier::fromString(vm, "console"_s)).getObject(); consoleObject->putDirectBuiltinFunction(vm, this, vm.propertyNames->asyncIteratorSymbol, consoleObjectAsyncIteratorCodeGenerator(vm), PropertyAttribute::Builtin | 0); consoleObject->putDirectBuiltinFunction(vm, this, clientData->builtinNames().writePublicName(), consoleObjectWriteCodeGenerator(vm), PropertyAttribute::Builtin | 0); - consoleObject->putDirectCustomAccessor(vm, Identifier::fromString(vm, "Console"_s), CustomGetterSetter::create(vm, getConsoleConstructor, nullptr), 0); + consoleObject->putDirectCustomAccessor(vm, Identifier::fromString(vm, "Console"_s), CustomGetterSetter::create(vm, getConsoleConstructor, noop_setter), 0); } extern "C" bool JSC__JSGlobalObject__startRemoteInspector(JSC__JSGlobalObject* globalObject, unsigned char* host, uint16_t arg1) @@ -3640,6 +3740,7 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) visitor.append(thisObject->m_nodeModuleOverriddenResolveFilename); visitor.append(thisObject->m_nextTickQueue); + visitor.append(thisObject->m_errorConstructorPrepareStackTraceValue); thisObject->m_JSArrayBufferSinkClassStructure.visit(visitor); thisObject->m_JSBufferListClassStructure.visit(visitor); diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index ad5138527..031dc4a13 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -152,8 +152,8 @@ public: void clearDOMGuardedObjects(); - static void createCallSitesFromFrames(JSC::JSGlobalObject* lexicalGlobalObject, JSCStackTrace& stackTrace, JSC::JSArray* callSites); - JSC::JSValue formatStackTrace(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSObject* errorObject, JSC::JSArray* callSites); + static void createCallSitesFromFrames(Zig::GlobalObject* globalObject, JSC::JSGlobalObject* lexicalGlobalObject, JSCStackTrace& stackTrace, JSC::JSArray* callSites); + void formatStackTrace(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSObject* errorObject, JSC::JSArray* callSites, JSValue prepareStack = JSC::jsUndefined()); static void reportUncaughtExceptionAtEventLoop(JSGlobalObject*, JSC::Exception*); static JSGlobalObject* deriveShadowRealmGlobalObject(JSGlobalObject* globalObject); @@ -351,6 +351,8 @@ public: // mutable WriteBarrier<Unknown> m_JSBunDebuggerValue; mutable WriteBarrier<JSFunction> m_thenables[promiseFunctionsSize + 1]; + mutable WriteBarrier<JSC::Unknown> m_errorConstructorPrepareStackTraceValue; + Structure* memoryFootprintStructure() { return m_memoryFootprintStructure.getInitializedOnMainThread(this); diff --git a/test/js/node/v8/capture-stack-trace.test.js b/test/js/node/v8/capture-stack-trace.test.js index cb2624681..c03491b33 100644 --- a/test/js/node/v8/capture-stack-trace.test.js +++ b/test/js/node/v8/capture-stack-trace.test.js @@ -17,6 +17,17 @@ test("Regular .stack", () => { expect(err.stack).toMatch(/at new Foo/); }); +test("throw inside Error.prepareStackTrace doesnt crash", () => { + + Error.prepareStackTrace = function (err, stack) { + Error.prepareStackTrace = null; + throw new Error('wat'); + }; + +expect(() => new Error().stack).toThrow("wat"); + +}); + test("capture stack trace", () => { function f1() { f2(); @@ -460,14 +471,52 @@ test("CallFrame.p.toString", () => { expect(e.stack[0].toString().includes("<anonymous>")).toBe(true); }); -test.todo("err.stack should invoke prepareStackTrace", () => { - // This is V8's behavior. - let prevPrepareStackTrace = Error.prepareStackTrace; - let wasCalled = false; - Error.prepareStackTrace = (e, s) => { - wasCalled = true; - }; - const e = new Error(); - e.stack; - expect(wasCalled).toBe(true); +test("err.stack should invoke prepareStackTrace", () => { + var lineNumber = -1; + var functionName = ""; + var parentLineNumber = -1; + function functionWithAName() { + // This is V8's behavior. + let prevPrepareStackTrace = Error.prepareStackTrace; + + Error.prepareStackTrace = (e, s) => { + lineNumber = s[0].getLineNumber(); + functionName = s[0].getFunctionName(); + parentLineNumber = s[1].getLineNumber(); + expect(s[0].getFileName().includes("capture-stack-trace.test.js")).toBe(true); + expect(s[1].getFileName().includes("capture-stack-trace.test.js")).toBe(true); + }; + const e = new Error(); + e.stack; + Error.prepareStackTrace = prevPrepareStackTrace; + } + + functionWithAName(); + + expect(functionName).toBe("functionWithAName"); + expect(lineNumber).toBe(490); + // TODO: this is wrong + expect(parentLineNumber).toBe(499); +}); + + +test("Error.prepareStackTrace inside a node:vm works", () => { + const {runInNewContext} = require("node:vm"); + Error.prepareStackTrace = null; + const result = runInNewContext( + ` + Error.prepareStackTrace = (err, stack) => { + if (typeof err.stack !== "string") { + throw new Error("err.stack is not a string"); + } + + return "custom stack trace"; + }; + + const err = new Error(); + err.stack; + ` + ) + expect(result).toBe("custom stack trace"); + expect(Error.prepareStackTrace).toBeNull(); }); diff --git a/test/js/node/vm/vm.test.ts b/test/js/node/vm/vm.test.ts index 510448c5e..79becfed0 100644 --- a/test/js/node/vm/vm.test.ts +++ b/test/js/node/vm/vm.test.ts @@ -101,7 +101,7 @@ function testRunInContext( expect(typeof result).toBe("function"); expect(result()).toBe("bar"); }); - test.skip("can throw a syntax error", () => { + test("can throw a syntax error", () => { const context = createContext({}); const result = () => fn("!?", context); expect(result).toThrow({ |