WTF::String Bun::formatStackTrace()

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();
}