in src/bun.js/bindings/ZigGlobalObject.cpp [350:567]
WTF::String Bun::formatStackTrace(
JSC::VM& vm,
Zig::GlobalObject* globalObject,
JSC::JSGlobalObject* lexicalGlobalObject,
const WTF::String& name,
const WTF::String& message,
OrdinalNumber& line,
OrdinalNumber& column,
WTF::String& sourceURL,
Vector<JSC::StackFrame>& stackTrace,
JSC::JSObject* errorInstance)
{
WTF::StringBuilder sb;
if (!name.isEmpty()) {
sb.append(name);
if (!message.isEmpty()) {
sb.append(": "_s);
sb.append(message);
}
} else if (!message.isEmpty()) {
sb.append(message);
}
// FIXME: why can size == 6 and capacity == 0?
// https://discord.com/channels/876711213126520882/1174901590457585765/1174907969419350036
size_t framesCount = stackTrace.size();
bool hasSet = false;
if (errorInstance) {
if (JSC::ErrorInstance* err = jsDynamicCast<JSC::ErrorInstance*>(errorInstance)) {
if (err->errorType() == ErrorType::SyntaxError && (stackTrace.isEmpty() || stackTrace.at(0).sourceURL(vm) != err->sourceURL())) {
// There appears to be an off-by-one error.
// The following reproduces the issue:
// /* empty comment */
// "".test(/[a-0]/);
auto originalLine = WTF::OrdinalNumber::fromOneBasedInt(err->line());
ZigStackFrame remappedFrame = {};
memset(&remappedFrame, 0, sizeof(ZigStackFrame));
remappedFrame.position.line_zero_based = originalLine.zeroBasedInt();
remappedFrame.position.column_zero_based = 0;
String sourceURLForFrame = err->sourceURL();
// If it's not a Zig::GlobalObject, don't bother source-mapping it.
if (globalObject && !sourceURLForFrame.isEmpty()) {
// https://github.com/oven-sh/bun/issues/3595
if (!sourceURLForFrame.isEmpty()) {
remappedFrame.source_url = Bun::toStringRef(sourceURLForFrame);
// This ensures the lifetime of the sourceURL is accounted for correctly
Bun__remapStackFramePositions(globalObject, &remappedFrame, 1);
sourceURLForFrame = remappedFrame.source_url.toWTFString();
}
}
// there is always a newline before each stack frame line, ensuring that the name + message
// exist on the first line, even if both are empty
sb.append("\n"_s);
sb.append(" at <parse> ("_s);
sb.append(remappedFrame.source_url.toWTFString());
if (remappedFrame.remapped) {
errorInstance->putDirect(vm, builtinNames(vm).originalLinePublicName(), jsNumber(originalLine.oneBasedInt()), 0);
hasSet = true;
line = remappedFrame.position.line();
}
if (remappedFrame.remapped) {
sb.append(":"_s);
sb.append(remappedFrame.position.line().oneBasedInt());
} else {
sb.append(":"_s);
sb.append(originalLine.oneBasedInt());
}
sb.append(")"_s);
}
}
}
if (framesCount == 0) {
ASSERT(stackTrace.isEmpty());
return sb.toString();
}
sb.append("\n"_s);
for (size_t i = 0; i < framesCount; i++) {
StackFrame& frame = stackTrace.at(i);
sb.append(" at "_s);
WTF::String functionName;
if (auto codeblock = frame.codeBlock()) {
if (codeblock->isConstructor()) {
sb.append("new "_s);
}
// We cannot run this in FinalizeUnconditionally, as we cannot call getters there
// We check the errorInstance to see if we are allowed to access this memory.
if (errorInstance) {
switch (codeblock->codeType()) {
case JSC::CodeType::FunctionCode:
case JSC::CodeType::EvalCode: {
if (auto* callee = frame.callee()) {
if (callee->isObject()) {
JSValue functionNameValue = callee->getObject()->getDirect(vm, vm.propertyNames->name);
if (functionNameValue && functionNameValue.isString()) {
functionName = functionNameValue.toWTFString(lexicalGlobalObject);
}
}
}
break;
}
default: {
break;
}
}
}
}
if (functionName.isEmpty()) {
functionName = frame.functionName(vm);
}
if (functionName.isEmpty()) {
sb.append("<anonymous>"_s);
} else {
sb.append(functionName);
}
if (frame.hasLineAndColumnInfo()) {
ZigStackFrame remappedFrame = {};
LineColumn lineColumn = frame.computeLineAndColumn();
OrdinalNumber originalLine = OrdinalNumber::fromOneBasedInt(lineColumn.line);
OrdinalNumber originalColumn = OrdinalNumber::fromOneBasedInt(lineColumn.column);
remappedFrame.position.line_zero_based = originalLine.zeroBasedInt();
remappedFrame.position.column_zero_based = originalColumn.zeroBasedInt();
String sourceURLForFrame = frame.sourceURL(vm);
// Sometimes, the sourceURL is empty.
// For example, pages in Next.js.
if (sourceURLForFrame.isEmpty()) {
// hasLineAndColumnInfo() checks codeBlock(), so this is safe to access here.
const auto& source = frame.codeBlock()->source();
// source.isNull() is true when the SourceProvider is a null pointer.
if (!source.isNull()) {
auto* provider = source.provider();
// I'm not 100% sure we should show sourceURLDirective here.
if (!provider->sourceURLDirective().isEmpty()) {
sourceURLForFrame = provider->sourceURLDirective();
} else if (!provider->sourceURL().isEmpty()) {
sourceURLForFrame = provider->sourceURL();
} else {
const auto& origin = provider->sourceOrigin();
if (!origin.isNull()) {
sourceURLForFrame = origin.string();
}
}
}
}
// If it's not a Zig::GlobalObject, don't bother source-mapping it.
if (globalObject == lexicalGlobalObject && globalObject) {
// https://github.com/oven-sh/bun/issues/3595
if (!sourceURLForFrame.isEmpty()) {
remappedFrame.source_url = Bun::toStringRef(sourceURLForFrame);
// This ensures the lifetime of the sourceURL is accounted for correctly
Bun__remapStackFramePositions(globalObject, &remappedFrame, 1);
sourceURLForFrame = remappedFrame.source_url.toWTFString();
}
}
if (!hasSet) {
hasSet = true;
line = remappedFrame.position.line();
column = remappedFrame.position.column();
sourceURL = frame.sourceURL(vm);
if (remappedFrame.remapped) {
if (errorInstance) {
errorInstance->putDirect(vm, builtinNames(vm).originalLinePublicName(), jsNumber(originalLine.oneBasedInt()), 0);
errorInstance->putDirect(vm, builtinNames(vm).originalColumnPublicName(), jsNumber(originalColumn.oneBasedInt()), 0);
}
}
}
sb.append(" ("_s);
sb.append(sourceURLForFrame);
sb.append(":"_s);
sb.append(remappedFrame.position.line().oneBasedInt());
sb.append(":"_s);
sb.append(remappedFrame.position.column().oneBasedInt());
sb.append(")"_s);
} else {
sb.append(" (native)"_s);
}
if (i != framesCount - 1) {
sb.append("\n"_s);
}
}
return sb.toString();
}