Skip to content

Commit

Permalink
lib: add util.getCallSite() API
Browse files Browse the repository at this point in the history
  • Loading branch information
RafaelGSS committed Aug 20, 2024
1 parent 9b3d22d commit 8c114c9
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 0 deletions.
54 changes: 54 additions & 0 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,60 @@ util.formatWithOptions({ colors: true }, 'See object %O', { foo: 42 });
// when printed to a terminal.
```

## `util.getCallSite()`

> Stability: 1.1 - Active development
<!-- YAML
added: REPLACEME
-->

* Returns: {Object\[]} An array of stacktrace objects
* `functionName` {string} Returns the name of the function associated with this stack frame.
* `lineNumber` {string} Returns the number, 1-based, of the line for the associate function call.
* `lineNumber` {string} Returns the number, 1-based, of the line for the associate function call.
* `column` {number} Returns the 1-based column offset on the line for the associated function call.

Returns an array of stacktrace objects containing the stack of
the caller function.

```js
const util = require('node:util');

function exampleFunction() {
const callSites = util.getCallSite();

console.log('Call Sites:');
callSites.forEach((callSite, index) => {
console.log(`CallSite ${index + 1}:`);
console.log(`Function Name: ${callSite.functionName}`);
console.log(`Script Name: ${callSite.scriptName}`);
console.log(`Line Number: ${callSite.lineNumer}`);
console.log(`Column Number: ${callSite.column}`);
});
// CallSite 1:
// Function Name: exampleFunction
// Script Name: /home/example.js
// Line Number: 5
// Column Number: 26

// CallSite 2:
// Function Name: anotherFunction
// Script Name: /home/example.js
// Line Number: 22
// Column Number: 3

// ...
}

// A function to simulate another stack layer
function anotherFunction() {
exampleFunction();
}

anotherFunction();
```

## `util.getSystemErrorName(err)`

<!-- YAML
Expand Down
9 changes: 9 additions & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,14 @@ function parseEnv(content) {
return binding.parseEnv(content);
}

/**
* Returns the callSite
* @returns {object}
*/
function getCallSite() {
return binding.getCallSite();
};

// Keep the `exports =` so that various functions can still be monkeypatched
module.exports = {
_errnoException,
Expand All @@ -289,6 +297,7 @@ module.exports = {
format,
styleText,
formatWithOptions,
getCallSite,
getSystemErrorMap,
getSystemErrorName,
inherits,
Expand Down
4 changes: 4 additions & 0 deletions src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
"transferList") \
V(clone_untransferable_str, "Found invalid value in transferList.") \
V(code_string, "code") \
V(column_string, "column") \
V(commonjs_string, "commonjs") \
V(config_string, "config") \
V(constants_string, "constants") \
Expand Down Expand Up @@ -163,6 +164,7 @@
V(fragment_string, "fragment") \
V(frames_received_string, "framesReceived") \
V(frames_sent_string, "framesSent") \
V(function_name_string, "functionName") \
V(function_string, "function") \
V(get_string, "get") \
V(get_data_clone_error_string, "_getDataCloneError") \
Expand Down Expand Up @@ -212,6 +214,7 @@
V(kind_string, "kind") \
V(length_string, "length") \
V(library_string, "library") \
V(line_number_string, "lineNumber") \
V(mac_string, "mac") \
V(max_buffer_string, "maxBuffer") \
V(max_concurrent_streams_string, "maxConcurrentStreams") \
Expand Down Expand Up @@ -301,6 +304,7 @@
V(salt_length_string, "saltLength") \
V(scheme_string, "scheme") \
V(scopeid_string, "scopeid") \
V(script_name_string, "scriptName") \
V(serial_number_string, "serialNumber") \
V(serial_string, "serial") \
V(servername_string, "servername") \
Expand Down
42 changes: 42 additions & 0 deletions src/node_util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -254,12 +254,53 @@ static void ParseEnv(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(dotenv.ToObject(env));
}

static void GetCallSite(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();

Local<StackTrace> stack =
StackTrace::CurrentStackTrace(isolate, env->stack_trace_limit());
Local<Array> callsites = Array::New(isolate);

// Frame 0 is node:util. It should be skipped.
for (int i = 1; i < stack->GetFrameCount(); ++i) {
Local<Object> obj = Object::New(isolate);
Local<StackFrame> stack_frame = stack->GetFrame(isolate, i);

Utf8Value function_name(isolate, stack_frame->GetFunctionName());
Utf8Value script_name(isolate, stack_frame->GetScriptName());

obj->Set(env->context(),
env->function_name_string(),
String::NewFromUtf8(isolate, *function_name).ToLocalChecked())
.Check();
obj->Set(env->context(),
env->script_name_string(),
String::NewFromUtf8(isolate, *script_name).ToLocalChecked())
.Check();
obj->Set(env->context(),
env->line_number_string(),
Integer::NewFromUnsigned(isolate, stack_frame->GetLineNumber()))
.Check();
obj->Set(env->context(),
env->column_string(),
Integer::NewFromUnsigned(isolate, stack_frame->GetColumn()))
.Check();
if (callsites->Set(env->context(), callsites->Length(), obj).IsNothing()) {
args.GetReturnValue().Set(Array::New(isolate));
return;
}
}
args.GetReturnValue().Set(callsites);
}

void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(GetPromiseDetails);
registry->Register(GetProxyDetails);
registry->Register(GetCallerLocation);
registry->Register(IsArrayBufferDetached);
registry->Register(PreviewEntries);
registry->Register(GetCallSite);
registry->Register(GetOwnNonIndexProperties);
registry->Register(GetConstructorName);
registry->Register(GetExternalValue);
Expand Down Expand Up @@ -365,6 +406,7 @@ void Initialize(Local<Object> target,
SetMethodNoSideEffect(
context, target, "getConstructorName", GetConstructorName);
SetMethodNoSideEffect(context, target, "getExternalValue", GetExternalValue);
SetMethod(context, target, "getCallSite", GetCallSite);
SetMethod(context, target, "sleep", Sleep);
SetMethod(context, target, "parseEnv", ParseEnv);

Expand Down
4 changes: 4 additions & 0 deletions test/fixtures/get-call-site.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const util = require('node:util');
const assert = require('node:assert');
assert.ok(util.getCallSite().length > 1);
process.stdout.write(util.getCallSite()[0].scriptName);
55 changes: 55 additions & 0 deletions test/parallel/test-util-getCallSite.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use strict';

require('../common');

const fixtures = require('../common/fixtures');
const file = fixtures.path('get-call-site.js');

const { getCallSite } = require('node:util');
const { spawnSync } = require('node:child_process');
const assert = require('node:assert');

{
const callsite = getCallSite();
assert.ok(callsite.length > 1);
assert.match(
callsite[0].scriptName,
/test-util-getCallSite/,
'node:util should be ignored',
);
}


{
const { status, stderr, stdout } = spawnSync(
process.execPath,
[
'-e',
`const util = require('util');
const assert = require('assert');
assert.ok(util.getCallSite().length > 1);
process.stdout.write(util.getCallSite()[0].scriptName);
`,
],
);
assert.strictEqual(status, 0, stderr.toString());
assert.strictEqual(stdout.toString(), '[eval]');
}

{
const { status, stderr, stdout } = spawnSync(
process.execPath,
[file],
);
assert.strictEqual(status, 0, stderr.toString());
assert.strictEqual(stdout.toString(), file);
}

{
const originalStackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = 0;
const callsite = getCallSite();
// Error.stackTraceLimit should not influence callsite size
assert.notStrictEqual(callsite.length, 0);
Error.stackTraceLimit = originalStackTraceLimit;
}
2 changes: 2 additions & 0 deletions tools/doc/type-parser.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ const customTypesMap = {
'worker_threads.html#class-broadcastchannel-' +
'extends-eventtarget',

'CallSite': 'https://v8.dev/docs/stack-trace-api#customizing-stack-traces',

'Iterable':
`${jsDocPrefix}Reference/Iteration_protocols#The_iterable_protocol`,
'Iterator':
Expand Down

0 comments on commit 8c114c9

Please sign in to comment.