Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
24 changes: 24 additions & 0 deletions src/jsc/bindings/ErrorStackTrace.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include <JavaScriptCore/FunctionCodeBlock.h>

#include "ErrorStackFrame.h"
#include "InternalModuleRegistry.h"

using namespace JSC;
using namespace WebCore;
Expand Down Expand Up @@ -414,6 +415,29 @@ String sourceURL(JSC::VM& vm, const JSC::StackFrame& frame)
return sourceURL(frame.codeBlock());
}

// "bun:main" and "bun:wrap" are generated around the program. They are not builtin functions.
static bool isGeneratedModuleURL(const String& sourceURL)
{
return sourceURL == "bun:main"_s || sourceURL == "bun:wrap"_s;
}

bool isScriptFrame(const JSC::StackFrame& frame)
{
// A host function and a wasm function have no code block. Every module built into Bun is
// compiled as a builtin function, whatever it is named ("ws").
Comment thread
robobun marked this conversation as resolved.
Resolved
auto* codeBlock = frame.codeBlock();
if (!codeBlock || codeBlock->unlinkedCodeBlock()->isBuiltinFunction())
return false;

String url = sourceURL(codeBlock);
return !url.isEmpty() && !isGeneratedModuleURL(url);
}

bool isScriptSourceURL(const String& sourceURL)
{
return !sourceURL.isEmpty() && !isGeneratedModuleURL(sourceURL) && !Bun::isInternalModuleName(sourceURL);
}

String sourceURL(JSC::StackVisitor& visitor)
{
switch (visitor->codeType()) {
Expand Down
7 changes: 7 additions & 0 deletions src/jsc/bindings/ErrorStackTrace.h
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,13 @@ String sourceURL(JSC::VM& vm, const JSC::StackFrame& frame);
String sourceURL(JSC::StackVisitor& visitor);
String sourceURL(JSC::VM& vm, JSC::JSFunction* function);

// Whether `frame` runs the program's own code: JavaScript that is neither one of JavaScriptCore's
// builtins nor a module built into Bun.
Comment thread
robobun marked this conversation as resolved.
Resolved
bool isScriptFrame(const JSC::StackFrame&);
// The same question when only the frame's source URL is left, as on an Error whose frames were
// already formatted.
Comment thread
robobun marked this conversation as resolved.
Resolved
bool isScriptSourceURL(const String& sourceURL);

class FunctionNameFlags {
public:
static constexpr unsigned None = 0;
Expand Down
17 changes: 11 additions & 6 deletions src/jsc/bindings/FormatStackTraceForJS.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -664,13 +664,8 @@ WTF::String computeErrorInfoWrapperToString(JSC::VM& vm, Vector<StackFrame>& sta
return result;
}

void computeLineColumnWithSourcemap(JSC::VM& vm, JSC::SourceProvider* _Nonnull sourceProvider, JSC::LineColumn& lineColumn, WTF::String& remappedSourceURL)
void remapLineColumn(JSC::VM& vm, const WTF::String& sourceURL, JSC::LineColumn& lineColumn, WTF::String& remappedSourceURL)
{
auto sourceURL = sourceProvider->sourceURL();
if (sourceURL.isEmpty()) {
return;
}

OrdinalNumber line = OrdinalNumber::fromOneBasedInt(lineColumn.line);
OrdinalNumber column = OrdinalNumber::fromOneBasedInt(lineColumn.column);

Expand All @@ -689,6 +684,16 @@ void computeLineColumnWithSourcemap(JSC::VM& vm, JSC::SourceProvider* _Nonnull s
}
}

void computeLineColumnWithSourcemap(JSC::VM& vm, JSC::SourceProvider* _Nonnull sourceProvider, JSC::LineColumn& lineColumn, WTF::String& remappedSourceURL)
{
const auto& sourceURL = sourceProvider->sourceURL();
if (sourceURL.isEmpty()) {
return;
}

remapLineColumn(vm, sourceURL, lineColumn, remappedSourceURL);
}

// ErrorInstance holds a frame's callee and code block weakly. Root them while user JS can run.
static void protectFrameCells(JSC::MarkedArgumentBuffer& cells, const Vector<StackFrame>& stackTrace)
{
Expand Down
3 changes: 3 additions & 0 deletions src/jsc/bindings/FormatStackTraceForJS.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ JSC_DECLARE_CUSTOM_SETTER(errorInstanceLazyStackCustomSetter);
WTF::String computeErrorInfoWrapperToString(JSC::VM& vm, WTF::Vector<JSC::StackFrame>& stackTrace, unsigned int& line_in, unsigned int& column_in, WTF::String& sourceURL);
JSC::JSValue computeErrorInfoWrapperToJSValue(JSC::VM& vm, WTF::Vector<JSC::StackFrame>& stackTrace, unsigned int& line_in, unsigned int& column_in, WTF::String& sourceURL, JSC::JSObject* errorInstance);
void computeLineColumnWithSourcemap(JSC::VM& vm, JSC::SourceProvider* _Nonnull sourceProvider, JSC::LineColumn& lineColumn, WTF::String& remappedSourceURL);
// Maps a 1-based position in the code that runs for `sourceURL` back to the source it was transpiled
// from. Leaves `lineColumn` and `remappedSourceURL` alone when that code has no source map.
Comment thread
robobun marked this conversation as resolved.
Resolved
void remapLineColumn(JSC::VM&, const WTF::String& sourceURL, JSC::LineColumn&, WTF::String& remappedSourceURL);
} // namespace Bun

namespace Zig {
Expand Down
10 changes: 10 additions & 0 deletions src/jsc/bindings/InternalModuleRegistry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ static String internalModuleString(uint32_t offset, uint32_t length)
return WTF::StringImpl::createWithoutCopying(std::span<const char>(bun_internal_modules_data + offset, length));
}

bool isInternalModuleName(StringView name)
{
for (uint32_t id = 0; id < bun_internal_modules_header.moduleCount; id++) {
const auto& m = internalModuleRecord(id);
if (name == StringView(std::span<const char>(bun_internal_modules_data + m.nameOffset, m.nameLength)))
return true;
}
return false;
}

#ifdef BUN_DYNAMIC_JS_LOAD_PATH
static String internalModuleSource(uint32_t id)
{
Expand Down
3 changes: 3 additions & 0 deletions src/jsc/bindings/InternalModuleRegistry.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ class InternalModuleRegistry : public JSInternalFieldObjectImpl<BUN_INTERNAL_MOD
void finishCreation(VM&);
};

// Whether `name` is the source URL of one of the JS modules built into Bun: "node:events", "ws".
bool isInternalModuleName(StringView name);

JSC_DECLARE_HOST_FUNCTION(jsInternalModulesLoadedFromBytecode);
JSC_DECLARE_HOST_FUNCTION(jsInternalModuleBytecode);

Expand Down
84 changes: 84 additions & 0 deletions src/jsc/bindings/JSDOMExceptionHandling.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@
#include "ErrorCode.h"

#include "DOMException.h"
#include "ErrorStackTrace.h"
#include "FormatStackTraceForJS.h"
#include "JSDOMException.h"
#include "JSDOMExceptionHandling.h"
#include "JSDOMPromiseDeferred.h"

#include <JavaScriptCore/ErrorHandlingScope.h>
#include <JavaScriptCore/ErrorInstance.h>
#include <JavaScriptCore/Exception.h>
#include <JavaScriptCore/ExceptionHelpers.h>
#include <JavaScriptCore/ScriptCallStack.h>
Expand Down Expand Up @@ -111,6 +114,87 @@ String retrieveErrorMessage(JSGlobalObject& lexicalGlobalObject, VM& vm, JSValue
return errorMessage;
}

static ScriptPosition positionOfFrame(VM& vm, JSGlobalObject* globalObject, const StackFrame& frame)
{
LineColumn lineColumn = frame.computeLineAndColumn();
String sourceURL = Zig::sourceURL(vm, frame);

// A frame of a node:vm context keeps its own positions, as in formatStackTrace().
JSGlobalObject* globalObjectOfFrame = globalObject;
if (auto* callee = frame.callee()) {
if (auto* object = callee->getObject())
globalObjectOfFrame = object->globalObject();
}
if (globalObjectOfFrame == globalObject)
Bun::remapLineColumn(vm, String { sourceURL }, lineColumn, sourceURL);
return { sourceURL, lineColumn.line, lineColumn.column };
}

// The `sourceURL` own property: where an Error's source URL is once its frames were formatted,
// and what addErrorInfo() leaves on a DOMException that native code made.
Comment thread
robobun marked this conversation as resolved.
Resolved
static String ownSourceURL(VM& vm, JSObject* object)
{
JSValue sourceURL = object->getDirect(vm, vm.propertyNames->sourceURL);
return sourceURL && sourceURL.isString() ? asString(sourceURL)->tryGetValue() : String();
}

ScriptPosition scriptPositionOfThrownValue(JSGlobalObject& lexicalGlobalObject, JSValue thrownValue, JSC::Exception* exception)
{
auto& vm = JSC::getVM(&lexicalGlobalObject);
JSGlobalObject* globalObject = defaultGlobalObject(&lexicalGlobalObject);

// Where it was thrown: the first frame of the program still on the stack at the throw.
if (exception) {
for (const StackFrame& frame : exception->stack()) {
if (Zig::isScriptFrame(frame))
return positionOfFrame(vm, globalObject, frame);
}
}

// Else where the value was made, if it says.
if (auto* error = dynamicDowncast<ErrorInstance>(thrownValue)) {
auto* stackTrace = error->stackTrace();
if (!error->line()) {
if (!stackTrace)
return {};
// Reading `stack`, or a collection that finds one of these frames dead, replaces them
// with the position of the first one that has a position (formatStackTrace()). Report
// that frame or nothing, so that the answer is the same before and after.
Comment thread
robobun marked this conversation as resolved.
Resolved
for (const StackFrame& frame : *stackTrace) {
if (!frame.hasLineAndColumnInfo())
continue;
return Zig::isScriptFrame(frame) ? positionOfFrame(vm, globalObject, frame) : ScriptPosition {};
}
return {};
}

// The line and the column are the cell's: a program can write any value to the properties.
String sourceURL = ownSourceURL(vm, error);
if (!Zig::isScriptSourceURL(sourceURL))
return {};
if (!stackTrace)
return { sourceURL, error->line(), error->column() };
// Its frames were never formatted, so the line is the parser's, in the code that ran: a
// SyntaxError that only JavaScriptCore's parser finds. The parser gives no column.
Comment thread
robobun marked this conversation as resolved.
Resolved
LineColumn lineColumn { error->line(), 1 };
Bun::remapLineColumn(vm, String { sourceURL }, lineColumn, sourceURL);
return { sourceURL, lineColumn.line, 0 };
}

if (auto* domException = dynamicDowncast<JSDOMException>(thrownValue)) {
JSValue line = domException->getDirect(vm, vm.propertyNames->line);
JSValue column = domException->getDirect(vm, vm.propertyNames->column);
String sourceURL = ownSourceURL(vm, domException);
if (!line || !line.isUInt32() || !column || !column.isUInt32() || !Zig::isScriptSourceURL(sourceURL))
return {};
LineColumn lineColumn { line.asUInt32(), column.asUInt32() };
Bun::remapLineColumn(vm, String { sourceURL }, lineColumn, sourceURL);
return { sourceURL, lineColumn.line, lineColumn.column };
}

return {};
}

JSValue createDOMException(JSGlobalObject* lexicalGlobalObject, ExceptionCode ec, const String& message, const String& extra)
{
auto& vm = JSC::getVM(lexicalGlobalObject);
Expand Down
13 changes: 13 additions & 0 deletions src/jsc/bindings/JSDOMExceptionHandling.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include <JavaScriptCore/ThrowScope.h>

namespace JSC {
class Exception;
class TopExceptionScope;
}

Expand Down Expand Up @@ -58,6 +59,18 @@ WEBCORE_EXPORT JSC::EncodedJSValue rejectPromiseWithThisTypeError(DeferredPromis
WEBCORE_EXPORT JSC::EncodedJSValue rejectPromiseWithThisTypeError(JSC::JSGlobalObject&, ASCIILiteral interfaceName, ASCIILiteral operationName);

String retrieveErrorMessage(JSC::JSGlobalObject&, JSC::VM&, JSC::JSValue exception, JSC::TopExceptionScope&);

// Where the program was when an uncaught value came up: what an ErrorEvent reports as filename,
// lineno and colno. All empty / 0 when no frame of the program is known.
Comment thread
robobun marked this conversation as resolved.
Resolved
struct ScriptPosition {
// As `.stack` names the frame: a path for a module on disk, else its URL.
String sourceURL;
unsigned line { 0 };
unsigned column { 0 };
};
// `exception` is the JSC::Exception that carried `thrownValue`, or null when only the value is
// left. Runs no script.
Comment thread
robobun marked this conversation as resolved.
Resolved
ScriptPosition scriptPositionOfThrownValue(JSC::JSGlobalObject&, JSC::JSValue thrownValue, JSC::Exception*);
WEBCORE_EXPORT void reportException(JSC::JSGlobalObject*, JSC::JSValue exception, CachedScript* = nullptr, bool = false);
WEBCORE_EXPORT void reportException(JSC::JSGlobalObject*, JSC::Exception*, CachedScript* = nullptr, bool = false, ExceptionDetails* = nullptr);

Expand Down
24 changes: 19 additions & 5 deletions src/jsc/bindings/webcore/Worker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@
#include "ErrorEvent.h"
#include "Event.h"
#include "EventNames.h"
#include "JSDOMExceptionHandling.h"
#include "PathInlines.h"
#include "StructuredSerializeOptions.h"
#include <JavaScriptCore/Exception.h>
#include <JavaScriptCore/IteratorOperations.h>
#include <JavaScriptCore/ScriptCallStack.h>
#include <wtf/TZoneMallocInlines.h>
Expand Down Expand Up @@ -194,18 +197,29 @@ extern "C" void WebWorker__parentContextWillDestroy(WorkerMessagingProxy* proxy)
}

// An uncaught error inside the worker: dispatch 'error' on the worker's own global scope, then report
// it to the Worker object.
extern "C" void WebWorker__dispatchError(Zig::GlobalObject* globalObject, WorkerMessagingProxy* proxy, BunString message, JSC::EncodedJSValue errorValue)
// it to the Worker object. `errorValue` is the event's `error`. `thrown` is what came up: the
// JSC::Exception when the thrower still had it, whose stack says where it was thrown, else the value.
Comment thread
robobun marked this conversation as resolved.
Resolved
extern "C" void WebWorker__dispatchError(Zig::GlobalObject* globalObject, WorkerMessagingProxy* proxy, BunString message, JSC::EncodedJSValue errorValue, JSC::EncodedJSValue thrown)
{
JSC::JSValue error = JSC::JSValue::decode(errorValue);
String messageStr = message.transferToWTFString();

JSC::JSValue thrownValue = JSC::JSValue::decode(thrown);
auto* exception = dynamicDowncast<JSC::Exception>(thrownValue);
if (exception)
thrownValue = exception->value();
ScriptPosition position = scriptPositionOfThrownValue(*globalObject, thrownValue, exception);

ErrorEvent::Init init;
init.message = messageStr;
init.error = error;
// A module on disk has its path as its source URL.
init.filename = isAbsolutePath(position.sourceURL) ? URL::fileURLWithFileSystemPath(position.sourceURL).string() : position.sourceURL;
Comment thread
robobun marked this conversation as resolved.
Resolved
init.lineno = position.line;
init.colno = position.column;
init.error = JSC::JSValue::decode(errorValue);
init.cancelable = false;
init.bubbles = false;
globalObject->globalEventScope->dispatchEvent(ErrorEvent::create(eventNames().errorEvent, init, EventIsTrusted::Yes));
proxy->postErrorToWorkerObject(*globalObject, messageStr, error);
proxy->postErrorToWorkerObject(*globalObject, messageStr, init.filename, init.lineno, init.colno, init.error);
}

JSC_DECLARE_HOST_FUNCTION(jsFunctionSetParentPort);
Expand Down
13 changes: 8 additions & 5 deletions src/jsc/bindings/webcore/WorkerMessagingProxy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -450,14 +450,17 @@ void WorkerMessagingProxy::postMessageToWorkerObject(MessageWithMessagePorts&& m
}
}

void WorkerMessagingProxy::postMessageErrorToWorkerObject(String&& message)
void WorkerMessagingProxy::postMessageErrorToWorkerObject(String&& message, String&& filename, unsigned lineno, unsigned colno)
{
ScriptExecutionContext::postTaskTo(m_loaderContextIdentifier, m_loaderLoopKind, [protectedThis = Ref { *this }, message = WTF::move(message).isolatedCopy()](ScriptExecutionContext&) {
ScriptExecutionContext::postTaskTo(m_loaderContextIdentifier, m_loaderLoopKind, [protectedThis = Ref { *this }, message = WTF::move(message).isolatedCopy(), filename = WTF::move(filename).isolatedCopy(), lineno, colno](ScriptExecutionContext&) {
RefPtr workerObject = protectedThis->m_workerObject;
if (!workerObject)
return;
ErrorEvent::Init init;
init.message = message;
init.filename = filename;
init.lineno = lineno;
init.colno = colno;
workerObject->dispatchEvent(ErrorEvent::create(eventNames().errorEvent, init, EventIsTrusted::Yes));
});
}
Expand Down Expand Up @@ -503,15 +506,15 @@ bool WorkerMessagingProxy::postSerializedErrorToWorkerObject(Zig::GlobalObject&
});
}

void WorkerMessagingProxy::postErrorToWorkerObject(Zig::GlobalObject& workerGlobalObject, const String& message, JSC::JSValue error)
void WorkerMessagingProxy::postErrorToWorkerObject(Zig::GlobalObject& workerGlobalObject, const String& message, const String& filename, unsigned lineno, unsigned colno, JSC::JSValue error)
{
switch (m_options.kind) {
case WorkerOptions::Kind::Web:
postMessageErrorToWorkerObject(String { message });
postMessageErrorToWorkerObject(String { message }, String { filename }, lineno, colno);
return;
case WorkerOptions::Kind::Node:
if (!postSerializedErrorToWorkerObject(workerGlobalObject, error))
postMessageErrorToWorkerObject(String { message });
postMessageErrorToWorkerObject(String { message }, String { filename }, lineno, colno);
return;
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/jsc/bindings/webcore/WorkerMessagingProxy.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ class WorkerMessagingProxy final : public ThreadSafeRefCounted<WorkerMessagingPr
// The entry point has evaluated: Pending -> Running, queued tasks and messages are delivered.
void workerGlobalScopeStarted(Zig::GlobalObject&);
void postMessageToWorkerObject(MessageWithMessagePorts&&);
void postErrorToWorkerObject(Zig::GlobalObject&, const String& message, JSC::JSValue error);
// What the worker's own 'error' event said, for the Worker object's event.
void postErrorToWorkerObject(Zig::GlobalObject&, const String& message, const String& filename, unsigned lineno, unsigned colno, JSC::JSValue error);
// The thread's global scope, VM and per-thread state are gone; only the OS thread remains.
// stoppedByParent: it stopped because it was asked to and never called process.exit() itself.
void workerGlobalScopeDestroyed(int32_t exitCode, bool stoppedByParent);
Expand All @@ -126,7 +127,7 @@ class WorkerMessagingProxy final : public ThreadSafeRefCounted<WorkerMessagingPr
void releaseWorkerThread();
void drainMessagesToWorkerObject(ScriptExecutionContext&, DrainBudget);
void rejectAllCrossVMRequests();
void postMessageErrorToWorkerObject(String&& message);
void postMessageErrorToWorkerObject(String&& message, String&& filename, unsigned lineno, unsigned colno);
bool postSerializedErrorToWorkerObject(Zig::GlobalObject&, JSC::JSValue error);

// Parent thread only.
Expand Down
Loading
Loading