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
4 changes: 3 additions & 1 deletion src/codegen/builtins/host/process.builtins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,10 +343,12 @@ const queuesTick = [
refusal(["callback"], b.generic),
] satisfies b.FunctionField[];

const warningOptions = { type: b.any.optional, code: b.any.optional };
const warningOptions = { type: b.any.optional, code: b.any.optional, ctor: b.any.optional };
// `ctor` is looked for among the frames of the stack and never called.
const warningOptionUses = {
type: [formattedAfterReading("ctor", "code", "detail", "type")],
code: [formattedInErrorMessage],
ctor: b.inspected,
};

const emitsWarningOnNextTick = [
Expand Down
1 change: 0 additions & 1 deletion src/codegen/builtins/host/runtime-dependencies.builtins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,6 @@ const nativeCalls = [
{ file: "src/jsc/VirtualMachine.rs" },
{ file: inBindings("BunProcess.cpp"), times: 6 },
{ file: inBindings("BunProcess.h"), times: 2 },
{ file: inBindings("webcore/MessagePort.cpp") },
],
callers: [
{ file: "src/jsc/VirtualMachine.rs", caller: "unhandled_rejection_owned", times: 4, onBehalfOf: promises },
Expand Down
19 changes: 14 additions & 5 deletions src/jsc/bindings/BunProcess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2489,7 +2489,7 @@ JSValue Process::emitWarningErrorInstance(JSC::JSGlobalObject* lexicalGlobalObje
RETURN_IF_EXCEPTION(scope, {});
return jsUndefined();
}
__attribute__((minsize)) JSValue Process::emitWarning(JSC::JSGlobalObject* lexicalGlobalObject, JSValue warning, JSValue type, JSValue code, JSValue ctor)
__attribute__((minsize)) JSValue Process::emitWarning(JSC::JSGlobalObject* lexicalGlobalObject, JSValue warning, JSValue type, JSValue code, JSValue ctor, WarningStack warningStack)
{
Zig::GlobalObject* globalObject = defaultGlobalObject(lexicalGlobalObject);
VM& vm = getVM(globalObject);
Expand All @@ -2505,7 +2505,7 @@ __attribute__((minsize)) JSValue Process::emitWarning(JSC::JSGlobalObject* lexic
return jsUndefined();
}

if (!type.isNull() && type.isObject() && !isJSArray(type)) {
if (!type.isNull() && type.isObject() && !type.isCallable() && !isJSArray(type)) {
ctor = type.get(globalObject, Identifier::fromString(vm, "ctor"_s));
RETURN_IF_EXCEPTION(scope, {});

Expand Down Expand Up @@ -2545,7 +2545,15 @@ __attribute__((minsize)) JSValue Process::emitWarning(JSC::JSGlobalObject* lexic
if (warning.isString()) {
auto s = warning.getString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
errorInstance = createError(globalObject, !s.isEmpty() ? s : "Warning"_s);
String message = !s.isEmpty() ? s : "Warning"_s;
// Error.captureStackTrace takes a limit of 0, and one that is no number, for a limit of 10. createError makes no frames under such a limit.
if (warningStack == WarningStack::StartsBelowTopFrame && globalObject->stackTraceLimit().value_or(0)) {
// https://github.com/nodejs/node/blob/v26.3.0/lib/internal/process/warning.js#L187-L201. Not as node: the default cut is the host frame on top and not the value of process.emitWarning, and where the stack cannot be cut at ctor it is whole, because a tail call removes the frame of a ctor that ran.
errorInstance = ErrorInstance::create(globalObject, WTF::move(message), ErrorType::Error, {}, {}, {});
Bun::captureStackTrace(globalObject, errorInstance, ctor.isCallable() ? ctor : jsUndefined(), Zig::StackWithoutCut::IsWhole);
Comment thread
robobun marked this conversation as resolved.
Resolved
RETURN_IF_EXCEPTION(scope, {});
} else
errorInstance = createError(globalObject, message);
errorInstance->putDirect(vm, vm.propertyNames->name, type, JSC::PropertyAttribute::DontEnum | 0);
} else if (warning.isCell() && warning.asCell()->type() == ErrorInstanceType) {
errorInstance = warning.getObject();
Expand All @@ -2566,7 +2574,7 @@ JSC_DEFINE_HOST_FUNCTION(Process_emitWarning, (JSGlobalObject * lexicalGlobalObj
auto type = callFrame->argument(1);
auto code = callFrame->argument(2);
auto ctor = callFrame->argument(3);
return JSValue::encode(Process::emitWarning(globalObject, warning, type, code, ctor));
return JSValue::encode(Process::emitWarning(globalObject, warning, type, code, ctor, Process::WarningStack::StartsBelowTopFrame));
}

JSC_DEFINE_CUSTOM_GETTER(processExitCode, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName name))
Expand Down Expand Up @@ -4803,7 +4811,8 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionBinding, (JSGlobalObject * jsGlobalObje
jsString(vm, String("process.binding() is deprecated. Please use public APIs instead."_s)),
jsString(vm, String("DeprecationWarning"_s)),
jsString(vm, String("DEP0111"_s)),
jsUndefined());
jsUndefined(),
Process::WarningStack::StartsBelowTopFrame);
RETURN_IF_EXCEPTION(throwScope, {});
}
}
Expand Down
7 changes: 6 additions & 1 deletion src/jsc/bindings/BunProcess.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,12 @@ class Process : public WebCore::JSEventEmitter {
JSObject* ensureOnWarning(Zig::GlobalObject*);

static JSValue emitWarningErrorInstance(JSC::JSGlobalObject* lexicalGlobalObject, JSValue errorInstance);
static JSValue emitWarning(JSC::JSGlobalObject* lexicalGlobalObject, JSValue warning, JSValue type, JSValue code, JSValue ctor);
// StartsBelowTopFrame: the top frame is the host function that warns, and in node the stack starts at its caller.
enum class WarningStack : bool {
StartsAtTopFrame,
StartsBelowTopFrame,
};
static JSValue emitWarning(JSC::JSGlobalObject* lexicalGlobalObject, JSValue warning, JSValue type, JSValue code, JSValue ctor, WarningStack = WarningStack::StartsAtTopFrame);

JSString* cachedCwd() { return m_cachedCwd.get(); }
void setCachedCwd(JSC::VM& vm, JSString* cwd) { m_cachedCwd.set(vm, this, cwd); }
Expand Down
33 changes: 21 additions & 12 deletions src/jsc/bindings/ErrorStackTrace.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,13 @@ static bool severalEntriesResumedAsyncFunctions(JSC::VM& vm)
return false;
}

void JSCStackTrace::getFramesForCaller(JSC::VM& vm, JSC::CallFrame* callFrame, JSC::JSCell* owner, JSC::JSValue caller, WTF::Vector<JSC::StackFrame>& stackTrace, size_t stackTraceLimit)
static bool runsBodyOfGeneratorOrAsyncFunction(const JSC::StackFrame& frame)
{
auto* codeBlock = frame.codeBlock();
return codeBlock && JSC::isGeneratorOrAsyncFunctionBodyParseMode(codeBlock->unlinkedCodeBlock()->parseMode());
}

void JSCStackTrace::getFramesForCaller(JSC::VM& vm, JSC::CallFrame* callFrame, JSC::JSCell* owner, JSC::JSValue caller, WTF::Vector<JSC::StackFrame>& stackTrace, size_t stackTraceLimit, StackWithoutCut stackWithoutCut)
{
UNUSED_PARAM(callFrame);

Expand Down Expand Up @@ -180,6 +186,9 @@ void JSCStackTrace::getFramesForCaller(JSC::VM& vm, JSC::CallFrame* callFrame, J
return true;
}
#endif
// A name finds the frame of a resumed async function or generator, whose callee is another cell. For any other frame it is a guess, and a wrong guess removes frames that IsWhole keeps.
if (stackWithoutCut == StackWithoutCut::IsWhole && !runsBodyOfGeneratorOrAsyncFunction(frame))
return false;
if (!nameOfCaller)
nameOfCaller = Zig::functionName(vm, callerObject);
const WTF::String& callerName = *nameOfCaller;
Expand Down Expand Up @@ -211,27 +220,29 @@ void JSCStackTrace::getFramesForCaller(JSC::VM& vm, JSC::CallFrame* callFrame, J
};
JSC::Interpreter::StackFrameFilter filter = isWanted;

auto withoutCut = [&] {
if (stackWithoutCut == StackWithoutCut::IsWhole)
return getFramesForCaller(vm, callFrame, owner, JSC::jsUndefined(), stackTrace, stackTraceLimit);
stackTrace.shrink(0);
};

constexpr size_t everyFrame = std::numeric_limits<size_t>::max();
size_t framesToCollect = stackTraceLimit == everyFrame || !callerObject ? stackTraceLimit : stackTraceLimit + 1;
for (;; framesToCollect = everyFrame) {
// Delegate to Interpreter::getStackTrace which includes async stack frames
// (from the await chain via getAsyncStackTrace). The previous hand-rolled
// StackVisitor::visit walk only collected synchronous frames.
//
// We always collect with framesToSkip=1 (to drop Error.captureStackTrace
// We always collect with framesToSkip=1 (to drop the capturing host function
// itself) and without the caller argument, because Interpreter::getStackTrace's
// built-in caller filtering skips entry-frame tracking during the skip phase,
// which loses async frames when the caller is the innermost sync frame.
hasFoundCaller = !callerObject;
stackTrace.shrink(0);
vm.interpreter.getStackTrace(owner, stackTrace, 1, framesToCollect, nullptr, nullptr, nullptr, &filter);

// Match V8: if the caller is not found anywhere in the sync portion of the
// stack, remove everything.
if (!hasFoundCaller) {
stackTrace.shrink(0);
return;
}
if (!hasFoundCaller)
return withoutCut();

bool hasEveryFrame = stackTrace.size() < framesToCollect;
if (hasEveryFrame)
Expand All @@ -255,10 +266,8 @@ void JSCStackTrace::getFramesForCaller(JSC::VM& vm, JSC::CallFrame* callFrame, J

// Match V8: remove all frames up to and including the caller.
if (callerObject) {
if (stackTrace.first().isAsyncFrame()) {
stackTrace.shrink(0);
return;
}
if (stackTrace.first().isAsyncFrame())
return withoutCut();
stackTrace.removeAt(0);
}

Expand Down
8 changes: 7 additions & 1 deletion src/jsc/bindings/ErrorStackTrace.h
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ class JSCStackFrame {
bool calculateSourcePositions();
};

// The stack where it cannot be cut at `caller`: `caller` is on no frame, or frames of suspended async functions lie in front of it. V8 has no frames for a `caller` that is on no frame.
enum class StackWithoutCut : bool {
IsEmpty,
IsWhole,
};

class JSCStackTrace {
private:
WTF::Vector<JSCStackFrame> m_frames;
Expand All @@ -182,7 +188,7 @@ class JSCStackTrace {
// Drops private-visibility frames (present when Options::showPrivateScriptsInStackTraces() is on), so the result does not index like existingFrames.
static JSCStackTrace fromExisting(JSC::VM& vm, const WTF::Vector<JSC::StackFrame>& existingFrames);

static void getFramesForCaller(JSC::VM& vm, JSC::CallFrame* callFrame, JSC::JSCell* owner, JSC::JSValue caller, WTF::Vector<JSC::StackFrame>& stackTrace, size_t stackTraceLimit);
static void getFramesForCaller(JSC::VM& vm, JSC::CallFrame* callFrame, JSC::JSCell* owner, JSC::JSValue caller, WTF::Vector<JSC::StackFrame>& stackTrace, size_t stackTraceLimit, StackWithoutCut = StackWithoutCut::IsEmpty);

private:
JSCStackTrace(WTF::Vector<JSCStackFrame>& frames)
Expand Down
25 changes: 15 additions & 10 deletions src/jsc/bindings/FormatStackTraceForJS.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -878,17 +878,24 @@ JSC_DEFINE_HOST_FUNCTION(errorConstructorFuncCaptureStackTrace, (JSC::JSGlobalOb
return JSC::JSValue::encode(throwTypeError(lexicalGlobalObject, scope, "invalid_argument"_s));
}

JSC::JSObject* errorObject = objectArg.asCell()->getObject();
JSC::JSValue caller = callFrame->argument(1);
captureStackTrace(globalObject, objectArg.asCell()->getObject(), callFrame->argument(1), Zig::StackWithoutCut::IsEmpty);
RETURN_IF_EXCEPTION(scope, {});
return JSC::JSValue::encode(JSC::jsUndefined());
}

void captureStackTrace(Zig::GlobalObject* globalObject, JSC::JSObject* errorObject, JSC::JSValue caller, Zig::StackWithoutCut stackWithoutCut)
{
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);

size_t stackTraceLimit = globalObject->stackTraceLimit().value_or(DEFAULT_ERROR_STACK_TRACE_LIMIT);
if (stackTraceLimit == 0) {
stackTraceLimit = DEFAULT_ERROR_STACK_TRACE_LIMIT;
}

WTF::Vector<JSC::StackFrame> stackTrace;
JSCStackTrace::getFramesForCaller(vm, callFrame, errorObject, caller, stackTrace, stackTraceLimit);
RETURN_IF_EXCEPTION(scope, {});
JSCStackTrace::getFramesForCaller(vm, nullptr, errorObject, caller, stackTrace, stackTraceLimit, stackWithoutCut);
RETURN_IF_EXCEPTION(scope, );

if (auto* instance = dynamicDowncast<JSC::ErrorInstance>(errorObject)) {
if (instance->hasMaterializedErrorInfo()) {
Expand All @@ -901,7 +908,7 @@ JSC_DEFINE_HOST_FUNCTION(errorConstructorFuncCaptureStackTrace, (JSC::JSGlobalOb
OrdinalNumber column;
String sourceURL;
JSValue result = computeErrorInfoToJSValue(vm, stackTrace, line, column, sourceURL, errorObject);
RETURN_IF_EXCEPTION(scope, {});
RETURN_IF_EXCEPTION(scope, );
errorObject->putDirect(vm, vm.propertyNames->stack, result, JSC::PropertyAttribute::DontEnum | 0);
} else {
// Not yet materialized — safe to install new frames with a lazy getter.
Expand All @@ -913,7 +920,7 @@ JSC_DEFINE_HOST_FUNCTION(errorConstructorFuncCaptureStackTrace, (JSC::JSGlobalOb
DeletePropertySlot slot;
JSObject::deleteProperty(instance, globalObject, propertyName, slot);
}
RETURN_IF_EXCEPTION(scope, {});
RETURN_IF_EXCEPTION(scope, );

instance->putDirectCustomAccessor(vm, vm.propertyNames->stack, globalObject->m_lazyStackCustomGetterSetter.get(globalObject), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::CustomAccessor | 0);
}
Expand All @@ -922,12 +929,10 @@ JSC_DEFINE_HOST_FUNCTION(errorConstructorFuncCaptureStackTrace, (JSC::JSGlobalOb
OrdinalNumber column;
String sourceURL;
JSValue result = computeErrorInfoToJSValue(vm, stackTrace, line, column, sourceURL, errorObject);
RETURN_IF_EXCEPTION(scope, {});
RETURN_IF_EXCEPTION(scope, );
putDirectOrThrow(globalObject, errorObject, vm.propertyNames->stack, result, JSC::PropertyAttribute::DontEnum | 0);
RETURN_IF_EXCEPTION(scope, {});
RETURN_IF_EXCEPTION(scope, );
}

return JSC::JSValue::encode(JSC::jsUndefined());
}

} // namespace Bun
Expand Down
4 changes: 4 additions & 0 deletions src/jsc/bindings/FormatStackTraceForJS.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class OrdinalNumber;
namespace Zig {
class GlobalObject;
class JSCStackTrace;
enum class StackWithoutCut : bool;
} // namespace Zig

using JSC::EncodedJSValue;
Expand All @@ -70,6 +71,9 @@ WTF::String formatStackTrace(
WTF::Vector<JSC::StackFrame>& stackTrace,
JSC::JSObject* errorInstance);

// Error.captureStackTrace(errorObject, caller) for a host function whose own frame is on top of the stack.
void captureStackTrace(Zig::GlobalObject*, JSC::JSObject* errorObject, JSC::JSValue caller, Zig::StackWithoutCut);

// JSC Host Functions - Error constructor methods
JSC_DECLARE_HOST_FUNCTION(errorConstructorFuncCaptureStackTrace);
JSC_DECLARE_HOST_FUNCTION(errorConstructorFuncAppendStackTrace);
Expand Down
3 changes: 2 additions & 1 deletion src/jsc/bindings/ProcessBindingUV.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@ JSC_DEFINE_HOST_FUNCTION(jsErrname, (JSGlobalObject * globalObject, JSC::CallFra
JSC::jsString(vm, WTF::String("Directly calling process.binding('uv').errname(<val>) is being deprecated. Please make sure to use util.getSystemErrorName() instead."_s)),
JSC::jsString(vm, WTF::String("DeprecationWarning"_s)),
JSC::jsString(vm, WTF::String("DEP0119"_s)),
JSC::jsUndefined());
JSC::jsUndefined(),
Process::WarningStack::StartsBelowTopFrame);
RETURN_IF_EXCEPTION(scope, {});
}
}
Expand Down
14 changes: 7 additions & 7 deletions src/jsc/bindings/webcore/MessagePort.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
#include <JavaScriptCore/JSArrayBuffer.h>

#include "BunClientData.h"
#include "BunProcess.h"
#include "EventNames.h"
#include "GlobalEventScope.h"
#include "JSMessagePort.h"
Expand All @@ -40,8 +41,6 @@
#include "WebCoreOpaqueRoot.h"
#include <wtf/TZoneMallocInlines.h>

extern "C" void Bun__Process__emitWarning(Zig::GlobalObject*, JSC::EncodedJSValue warning, JSC::EncodedJSValue type, JSC::EncodedJSValue code, JSC::EncodedJSValue ctor);

namespace WebCore {

WTF_MAKE_TZONE_ALLOCATED_IMPL(MessagePort);
Expand Down Expand Up @@ -128,11 +127,12 @@ ExceptionOr<void> MessagePort::postMessage(JSC::JSGlobalObject& state, JSC::JSVa

if (targetsEntangledPeer) {
// Node warns and loses the channel instead of throwing; close() stops the dead channel from reffing the loop.
Bun__Process__emitWarning(defaultGlobalObject(&state),
JSC::JSValue::encode(JSC::jsString(vm, String("The target port was posted to itself, and the communication channel was lost"_s))),
JSC::JSValue::encode(JSC::jsString(vm, String("Warning"_s))),
JSC::JSValue::encode(JSC::jsUndefined()),
JSC::JSValue::encode(JSC::jsUndefined()));
Bun::Process::emitWarning(&state,
JSC::jsString(vm, String("The target port was posted to itself, and the communication channel was lost"_s)),
JSC::jsString(vm, String("Warning"_s)),
JSC::jsUndefined(),
JSC::jsUndefined(),
Bun::Process::WarningStack::StartsBelowTopFrame);
CLEAR_IF_EXCEPTION(warnScope);
close();
RETURN_IF_EXCEPTION(warnScope, Exception { ExistingExceptionError });
Expand Down
Loading
Loading