diff --git a/src/bun.js/bindings/ErrorStackTrace.cpp b/src/bun.js/bindings/ErrorStackTrace.cpp index 2973aee680..32bffbaa42 100644 --- a/src/bun.js/bindings/ErrorStackTrace.cpp +++ b/src/bun.js/bindings/ErrorStackTrace.cpp @@ -268,18 +268,11 @@ JSCStackTrace JSCStackTrace::captureCurrentJSStackTrace(Zig::GlobalObject* globa bool belowCaller = false; int32_t skipFrames = 0; - WTF::String callerName {}; - if (JSC::JSFunction* callerFunction = JSC::jsDynamicCast(caller)) { - callerName = callerFunction->name(vm); - if (!callerFunction->name(vm).isEmpty() || callerFunction->isHostOrBuiltinFunction()) { - callerName = callerFunction->name(vm); - } else { - callerName = callerFunction->jsExecutable()->name().string(); + WTF::String callerName; + if (caller) + if (auto* object = caller.getObject()) { + callerName = Zig::functionName(vm, globalObject, object); } - } - if (JSC::InternalFunction* callerFunctionInternal = JSC::jsDynamicCast(caller)) { - callerName = callerFunctionInternal->name(); - } if (!callerName.isEmpty()) { JSC::StackVisitor::visit(callFrame, vm, [&](JSC::StackVisitor& visitor) -> WTF::IterationStatus { diff --git a/src/bun.js/bindings/NodeUtilModule.cpp b/src/bun.js/bindings/NodeUtilModule.cpp new file mode 100644 index 0000000000..3f24eded28 --- /dev/null +++ b/src/bun.js/bindings/NodeUtilModule.cpp @@ -0,0 +1,134 @@ +#include "GeneratedJS2Native.h" +#include "root.h" + +#include "ErrorStackTrace.h" +#include "ErrorCode.h" +#include +#include +#include +#include +#include +#include + +namespace Bun { +using namespace JSC; +using namespace ERR; +JSC_DEFINE_HOST_FUNCTION(jsFunctionUtilGetCallSites, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSC::JSValue firstArg = callFrame->argument(0); + JSC::JSValue secondArg = callFrame->argument(1); + + size_t frameLimit = 10; // Default frame limit + + if (secondArg.isUndefined() && firstArg.isObject()) { + secondArg = firstArg; + } else if (!firstArg.isUndefined()) { + if (!firstArg.isNumber()) { + return ERR::INVALID_ARG_TYPE(scope, globalObject, "frameCount"_s, "number"_s, firstArg); + } + int64_t frameCount = firstArg.toInt32(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + if (frameCount < 1 || frameCount > 200) { + return ERR::OUT_OF_RANGE(scope, globalObject, "frameCount"_s, "number"_s, firstArg); + } + frameLimit = frameCount; + } + + // We don't do anything with the sourceMap option but we do the validation still. + if (!secondArg.isUndefined()) { + auto* optionsObj = secondArg.getObject(); + if (!optionsObj || JSC::isJSArray(optionsObj)) { + return ERR::INVALID_ARG_TYPE(scope, globalObject, "options"_s, "object"_s, secondArg); + } + + // Validate sourceMap option if present + JSC::JSValue sourceMapValue = optionsObj->get(globalObject, JSC::Identifier::fromString(vm, "sourceMap"_s)); + RETURN_IF_EXCEPTION(scope, {}); + if (!sourceMapValue.isUndefined() && !sourceMapValue.isBoolean()) { + return ERR::INVALID_ARG_TYPE(scope, globalObject, "options.sourceMap"_s, "boolean"_s, sourceMapValue); + } + } + + // Create array to store call sites + JSC::JSArray* callSites = JSC::constructEmptyArray(globalObject, nullptr); + RETURN_IF_EXCEPTION(scope, {}); + + // Get the stack trace + Zig::JSCStackTrace stackTrace = Zig::JSCStackTrace::captureCurrentJSStackTrace( + jsCast(globalObject), + callFrame, + frameLimit + 1, // Add 1 to account for the current frame + jsUndefined()); + + // Convert stack frames to call site objects + Identifier functionNameProperty = Identifier::fromString(vm, "functionName"_s); + Identifier scriptNameProperty = Identifier::fromString(vm, "scriptName"_s); + Identifier lineNumberProperty = Identifier::fromString(vm, "lineNumber"_s); + Identifier columnProperty = vm.propertyNames->column; + auto createFirstCallSite = [&]() -> JSObject* { + auto* callSite = JSC::constructEmptyObject(vm, globalObject->nullPrototypeObjectStructure()); + auto& frame = stackTrace.frames()[0]; + // Set functionName + JSC::JSString* functionName = frame.functionName(); + callSite->putDirect(vm, functionNameProperty, functionName ? functionName : jsEmptyString(vm)); + + // Set scriptName (sourceURL) + JSC::JSString* scriptName = frame.sourceURL(); + callSite->putDirect(vm, scriptNameProperty, scriptName ? scriptName : jsEmptyString(vm)); + + // Get line and column numbers + if (auto* positions = frame.getSourcePositions()) { + // Line number (1-based) + callSite->putDirect(vm, lineNumberProperty, JSC::jsNumber(positions->line.oneBasedInt())); + + // Column number (1-based) + callSite->putDirect(vm, columnProperty, JSC::jsNumber(positions->column.oneBasedInt())); + } else { + // If no position info available, use 0 + callSite->putDirect(vm, lineNumberProperty, JSC::jsNumber(0)); + callSite->putDirect(vm, columnProperty, JSC::jsNumber(0)); + } + + return callSite; + }; + + switch (stackTrace.frames().size()) { + case 0: + break; + case 1: { + auto callSite = createFirstCallSite(); + callSites->push(globalObject, callSite); + break; + } + default: { + JSC::Structure* structure = nullptr; + + auto* firstCallSite = createFirstCallSite(); + structure = firstCallSite->structure(); + + for (unsigned i = 1; i < stackTrace.frames().size(); ++i) { + auto& frame = stackTrace.frames()[i]; + auto* callSite = JSC::constructEmptyObject(vm, structure); + JSC::JSString* functionName = frame.functionName(); + + JSC::JSString* scriptName = frame.sourceURL(); + callSite->putDirectOffset(vm, 0, functionName ? functionName : jsEmptyString(vm)); + callSite->putDirectOffset(vm, 1, scriptName ? scriptName : jsEmptyString(vm)); + if (auto* positions = frame.getSourcePositions()) { + callSite->putDirectOffset(vm, 2, JSC::jsNumber(positions->line.oneBasedInt())); + callSite->putDirectOffset(vm, 3, JSC::jsNumber(positions->column.oneBasedInt())); + } else { + callSite->putDirectOffset(vm, 2, JSC::jsNumber(0)); + callSite->putDirectOffset(vm, 3, JSC::jsNumber(0)); + } + callSites->push(globalObject, callSite); + } + } + } + + return JSC::JSValue::encode(callSites); +} +} diff --git a/src/bun.js/bindings/NodeUtilModule.h b/src/bun.js/bindings/NodeUtilModule.h new file mode 100644 index 0000000000..3cc5236cdf --- /dev/null +++ b/src/bun.js/bindings/NodeUtilModule.h @@ -0,0 +1,7 @@ + + +namespace Bun { + +JSC_DECLARE_HOST_FUNCTION(jsFunctionUtilGetCallSites); + +} diff --git a/test/js/node/test/parallel/test-util-getcallsites.js b/test/js/node/test/parallel/test-util-getcallsites.js new file mode 100644 index 0000000000..5b902ac1f2 --- /dev/null +++ b/test/js/node/test/parallel/test-util-getcallsites.js @@ -0,0 +1,195 @@ +'use strict'; + +const common = require('../common'); + +const fixtures = require('../common/fixtures'); +const file = fixtures.path('get-call-sites.js'); + +const { getCallSites } = require('node:util'); +const { spawnSync } = require('node:child_process'); +const assert = require('node:assert'); + +function main() { + +{ + const callSites = getCallSites(); + assert.ok(callSites.length > 1); + assert.match( + callSites[0].scriptName, + /test-util-getcallsites/, + 'node:util should be ignored' + ); +} + +{ + const callSites = getCallSites(3); + assert.strictEqual(callSites.length, 3); + assert.match( + callSites[0].scriptName, + /test-util-getcallsites/, + 'node:util should be ignored' + ); +} + +// Guarantee dot-left numbers are ignored +{ + const callSites = getCallSites(3.6); + assert.strictEqual(callSites.length, 3); +} + +{ + const callSites = getCallSites(3.4); + assert.strictEqual(callSites.length, 3); +} + +{ + assert.throws( + () => { + // Max than kDefaultMaxCallStackSizeToCapture + getCallSites(201); + }, + common.expectsError({ + code: 'ERR_OUT_OF_RANGE', + }) + ); + assert.throws( + () => { + getCallSites(-1); + }, + common.expectsError({ + code: 'ERR_OUT_OF_RANGE', + }) + ); + assert.throws( + () => { + getCallSites([]); + }, + common.expectsError({ + code: 'ERR_INVALID_ARG_TYPE', + }) + ); + assert.throws( + () => { + getCallSites({}, {}); + }, + common.expectsError({ + code: 'ERR_INVALID_ARG_TYPE', + }) + ); + assert.throws( + () => { + getCallSites(10, 10); + }, + common.expectsError({ + code: 'ERR_INVALID_ARG_TYPE', + }) + ); +} + +{ + const callSites = getCallSites(1); + assert.strictEqual(callSites.length, 1); + assert.match( + callSites[0].scriptName, + /test-util-getcallsites/, + 'node:util should be ignored' + ); +} + +// Guarantee [eval] will appear on stacktraces when using -e +{ + const { status, stderr, stdout } = spawnSync(process.execPath, [ + '-e', + `const util = require('util'); + const assert = require('assert'); + assert.ok(util.getCallSites().length > 1); + process.stdout.write(util.getCallSites()[0].scriptName); + `, + ]); + assert.strictEqual(status, 0, stderr.toString()); + assert.strictEqual(stdout.toString(), '[eval]'); +} + +// Guarantee the stacktrace[0] is the filename +{ + const { status, stderr, stdout } = spawnSync(process.execPath, [file]); + assert.strictEqual(status, 0, stderr.toString()); + assert.strictEqual(stdout.toString(), file); +} + +// Error.stackTraceLimit should not influence callsite size +{ + const originalStackTraceLimit = Error.stackTraceLimit; + Error.stackTraceLimit = 0; + const callSites = getCallSites(); + assert.notStrictEqual(callSites.length, 0); + Error.stackTraceLimit = originalStackTraceLimit; +} + +{ + const { status, stderr, stdout } = spawnSync(process.execPath, [ + '--no-warnings', + '--experimental-transform-types', + fixtures.path('typescript/ts/test-get-callsite.ts'), + ]); + + const output = stdout.toString(); + assert.strictEqual(stderr.toString(), ''); + assert.match(output, /lineNumber: 8/); + assert.match(output, /column: 18/); + assert.match(output, /test-get-callsite\.ts/); + assert.strictEqual(status, 0); +} + +{ + const { status, stderr, stdout } = spawnSync(process.execPath, [ + '--no-warnings', + '--experimental-transform-types', + '--no-enable-source-maps', + fixtures.path('typescript/ts/test-get-callsite.ts'), + ]); + + const output = stdout.toString(); + assert.strictEqual(stderr.toString(), ''); + // Line should be wrong when sourcemaps are disable + assert.match(output, /lineNumber: 2/); + assert.match(output, /column: 18/); + assert.match(output, /test-get-callsite\.ts/); + assert.strictEqual(status, 0); +} + +{ + // Source maps should be disabled when options.sourceMap is false + const { status, stderr, stdout } = spawnSync(process.execPath, [ + '--no-warnings', + '--experimental-transform-types', + fixtures.path('typescript/ts/test-get-callsite-explicit.ts'), + ]); + + const output = stdout.toString(); + assert.strictEqual(stderr.toString(), ''); + assert.match(output, /lineNumber: 2/); + assert.match(output, /column: 18/); + assert.match(output, /test-get-callsite-explicit\.ts/); + assert.strictEqual(status, 0); +} + +} + +function wrapLevelFour() { + return main(); +} + +function wrapLevelThree() { + return wrapLevelFour(); +} + +function wrapLevelTwo() { + return wrapLevelThree(); +} + +function wrapLevelOne() { + return wrapLevelTwo(); +} + +wrapLevelOne();