Compare commits

...

1 Commits

Author SHA1 Message Date
Jarred Sumner
b2de4074cc the tests don't pass 2025-01-11 00:00:29 -08:00
4 changed files with 340 additions and 11 deletions

View File

@@ -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<JSC::JSFunction*>(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<JSC::InternalFunction*>(caller)) {
callerName = callerFunctionInternal->name();
}
if (!callerName.isEmpty()) {
JSC::StackVisitor::visit(callFrame, vm, [&](JSC::StackVisitor& visitor) -> WTF::IterationStatus {

View File

@@ -0,0 +1,134 @@
#include "GeneratedJS2Native.h"
#include "root.h"
#include "ErrorStackTrace.h"
#include "ErrorCode.h"
#include <JavaScriptCore/JSArray.h>
#include <JavaScriptCore/JSCInlines.h>
#include <JavaScriptCore/JSObject.h>
#include <JavaScriptCore/JSString.h>
#include <JavaScriptCore/ObjectConstructor.h>
#include <cmath>
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<Zig::GlobalObject*>(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);
}
}

View File

@@ -0,0 +1,7 @@
namespace Bun {
JSC_DECLARE_HOST_FUNCTION(jsFunctionUtilGetCallSites);
}

View File

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