Skip to content

Commit 8c114c9

Browse files
committed
lib: add util.getCallSite() API
1 parent 9b3d22d commit 8c114c9

File tree

7 files changed

+170
-0
lines changed

7 files changed

+170
-0
lines changed

doc/api/util.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,60 @@ util.formatWithOptions({ colors: true }, 'See object %O', { foo: 42 });
364364
// when printed to a terminal.
365365
```
366366

367+
## `util.getCallSite()`
368+
369+
> Stability: 1.1 - Active development
370+
371+
<!-- YAML
372+
added: REPLACEME
373+
-->
374+
375+
* Returns: {Object\[]} An array of stacktrace objects
376+
* `functionName` {string} Returns the name of the function associated with this stack frame.
377+
* `lineNumber` {string} Returns the number, 1-based, of the line for the associate function call.
378+
* `lineNumber` {string} Returns the number, 1-based, of the line for the associate function call.
379+
* `column` {number} Returns the 1-based column offset on the line for the associated function call.
380+
381+
Returns an array of stacktrace objects containing the stack of
382+
the caller function.
383+
384+
```js
385+
const util = require('node:util');
386+
387+
function exampleFunction() {
388+
const callSites = util.getCallSite();
389+
390+
console.log('Call Sites:');
391+
callSites.forEach((callSite, index) => {
392+
console.log(`CallSite ${index + 1}:`);
393+
console.log(`Function Name: ${callSite.functionName}`);
394+
console.log(`Script Name: ${callSite.scriptName}`);
395+
console.log(`Line Number: ${callSite.lineNumer}`);
396+
console.log(`Column Number: ${callSite.column}`);
397+
});
398+
// CallSite 1:
399+
// Function Name: exampleFunction
400+
// Script Name: /home/example.js
401+
// Line Number: 5
402+
// Column Number: 26
403+
404+
// CallSite 2:
405+
// Function Name: anotherFunction
406+
// Script Name: /home/example.js
407+
// Line Number: 22
408+
// Column Number: 3
409+
410+
// ...
411+
}
412+
413+
// A function to simulate another stack layer
414+
function anotherFunction() {
415+
exampleFunction();
416+
}
417+
418+
anotherFunction();
419+
```
420+
367421
## `util.getSystemErrorName(err)`
368422

369423
<!-- YAML

lib/util.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,14 @@ function parseEnv(content) {
275275
return binding.parseEnv(content);
276276
}
277277

278+
/**
279+
* Returns the callSite
280+
* @returns {object}
281+
*/
282+
function getCallSite() {
283+
return binding.getCallSite();
284+
};
285+
278286
// Keep the `exports =` so that various functions can still be monkeypatched
279287
module.exports = {
280288
_errnoException,
@@ -289,6 +297,7 @@ module.exports = {
289297
format,
290298
styleText,
291299
formatWithOptions,
300+
getCallSite,
292301
getSystemErrorMap,
293302
getSystemErrorName,
294303
inherits,

src/env_properties.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
"transferList") \
9898
V(clone_untransferable_str, "Found invalid value in transferList.") \
9999
V(code_string, "code") \
100+
V(column_string, "column") \
100101
V(commonjs_string, "commonjs") \
101102
V(config_string, "config") \
102103
V(constants_string, "constants") \
@@ -163,6 +164,7 @@
163164
V(fragment_string, "fragment") \
164165
V(frames_received_string, "framesReceived") \
165166
V(frames_sent_string, "framesSent") \
167+
V(function_name_string, "functionName") \
166168
V(function_string, "function") \
167169
V(get_string, "get") \
168170
V(get_data_clone_error_string, "_getDataCloneError") \
@@ -212,6 +214,7 @@
212214
V(kind_string, "kind") \
213215
V(length_string, "length") \
214216
V(library_string, "library") \
217+
V(line_number_string, "lineNumber") \
215218
V(mac_string, "mac") \
216219
V(max_buffer_string, "maxBuffer") \
217220
V(max_concurrent_streams_string, "maxConcurrentStreams") \
@@ -301,6 +304,7 @@
301304
V(salt_length_string, "saltLength") \
302305
V(scheme_string, "scheme") \
303306
V(scopeid_string, "scopeid") \
307+
V(script_name_string, "scriptName") \
304308
V(serial_number_string, "serialNumber") \
305309
V(serial_string, "serial") \
306310
V(servername_string, "servername") \

src/node_util.cc

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,12 +254,53 @@ static void ParseEnv(const FunctionCallbackInfo<Value>& args) {
254254
args.GetReturnValue().Set(dotenv.ToObject(env));
255255
}
256256

257+
static void GetCallSite(const FunctionCallbackInfo<Value>& args) {
258+
Environment* env = Environment::GetCurrent(args);
259+
Isolate* isolate = env->isolate();
260+
261+
Local<StackTrace> stack =
262+
StackTrace::CurrentStackTrace(isolate, env->stack_trace_limit());
263+
Local<Array> callsites = Array::New(isolate);
264+
265+
// Frame 0 is node:util. It should be skipped.
266+
for (int i = 1; i < stack->GetFrameCount(); ++i) {
267+
Local<Object> obj = Object::New(isolate);
268+
Local<StackFrame> stack_frame = stack->GetFrame(isolate, i);
269+
270+
Utf8Value function_name(isolate, stack_frame->GetFunctionName());
271+
Utf8Value script_name(isolate, stack_frame->GetScriptName());
272+
273+
obj->Set(env->context(),
274+
env->function_name_string(),
275+
String::NewFromUtf8(isolate, *function_name).ToLocalChecked())
276+
.Check();
277+
obj->Set(env->context(),
278+
env->script_name_string(),
279+
String::NewFromUtf8(isolate, *script_name).ToLocalChecked())
280+
.Check();
281+
obj->Set(env->context(),
282+
env->line_number_string(),
283+
Integer::NewFromUnsigned(isolate, stack_frame->GetLineNumber()))
284+
.Check();
285+
obj->Set(env->context(),
286+
env->column_string(),
287+
Integer::NewFromUnsigned(isolate, stack_frame->GetColumn()))
288+
.Check();
289+
if (callsites->Set(env->context(), callsites->Length(), obj).IsNothing()) {
290+
args.GetReturnValue().Set(Array::New(isolate));
291+
return;
292+
}
293+
}
294+
args.GetReturnValue().Set(callsites);
295+
}
296+
257297
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
258298
registry->Register(GetPromiseDetails);
259299
registry->Register(GetProxyDetails);
260300
registry->Register(GetCallerLocation);
261301
registry->Register(IsArrayBufferDetached);
262302
registry->Register(PreviewEntries);
303+
registry->Register(GetCallSite);
263304
registry->Register(GetOwnNonIndexProperties);
264305
registry->Register(GetConstructorName);
265306
registry->Register(GetExternalValue);
@@ -365,6 +406,7 @@ void Initialize(Local<Object> target,
365406
SetMethodNoSideEffect(
366407
context, target, "getConstructorName", GetConstructorName);
367408
SetMethodNoSideEffect(context, target, "getExternalValue", GetExternalValue);
409+
SetMethod(context, target, "getCallSite", GetCallSite);
368410
SetMethod(context, target, "sleep", Sleep);
369411
SetMethod(context, target, "parseEnv", ParseEnv);
370412

test/fixtures/get-call-site.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
const util = require('node:util');
2+
const assert = require('node:assert');
3+
assert.ok(util.getCallSite().length > 1);
4+
process.stdout.write(util.getCallSite()[0].scriptName);
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use strict';
2+
3+
require('../common');
4+
5+
const fixtures = require('../common/fixtures');
6+
const file = fixtures.path('get-call-site.js');
7+
8+
const { getCallSite } = require('node:util');
9+
const { spawnSync } = require('node:child_process');
10+
const assert = require('node:assert');
11+
12+
{
13+
const callsite = getCallSite();
14+
assert.ok(callsite.length > 1);
15+
assert.match(
16+
callsite[0].scriptName,
17+
/test-util-getCallSite/,
18+
'node:util should be ignored',
19+
);
20+
}
21+
22+
23+
{
24+
const { status, stderr, stdout } = spawnSync(
25+
process.execPath,
26+
[
27+
'-e',
28+
`const util = require('util');
29+
const assert = require('assert');
30+
assert.ok(util.getCallSite().length > 1);
31+
process.stdout.write(util.getCallSite()[0].scriptName);
32+
`,
33+
],
34+
);
35+
assert.strictEqual(status, 0, stderr.toString());
36+
assert.strictEqual(stdout.toString(), '[eval]');
37+
}
38+
39+
{
40+
const { status, stderr, stdout } = spawnSync(
41+
process.execPath,
42+
[file],
43+
);
44+
assert.strictEqual(status, 0, stderr.toString());
45+
assert.strictEqual(stdout.toString(), file);
46+
}
47+
48+
{
49+
const originalStackTraceLimit = Error.stackTraceLimit;
50+
Error.stackTraceLimit = 0;
51+
const callsite = getCallSite();
52+
// Error.stackTraceLimit should not influence callsite size
53+
assert.notStrictEqual(callsite.length, 0);
54+
Error.stackTraceLimit = originalStackTraceLimit;
55+
}

tools/doc/type-parser.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ const customTypesMap = {
4949
'worker_threads.html#class-broadcastchannel-' +
5050
'extends-eventtarget',
5151

52+
'CallSite': 'https://v8.dev/docs/stack-trace-api#customizing-stack-traces',
53+
5254
'Iterable':
5355
`${jsDocPrefix}Reference/Iteration_protocols#The_iterable_protocol`,
5456
'Iterator':

0 commit comments

Comments
 (0)