Skip to content

Commit 51ba566

Browse files
legendecasmarco-ippolito
authored andcommitted
lib: decorate async stack trace in source maps
Decorate stack frame with 'async' and 'new' keywords based on the type of the call site info. PR-URL: #53860 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Michaël Zasso <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]>
1 parent 137a2e5 commit 51ba566

11 files changed

+166
-45
lines changed

‎lib/internal/source_map/prepare_stack_trace.js‎

Lines changed: 68 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ const{
2424
const{ fileURLToPath }=require('internal/url');
2525
const{ setGetSourceMapErrorSource }=internalBinding('errors');
2626

27+
constkStackLineAt='\n at ';
28+
2729
// Create a prettified stacktrace, inserting context from source maps
2830
// if possible.
2931
functionprepareStackTraceWithSourceMaps(error,trace){
@@ -40,75 +42,98 @@ function prepareStackTraceWithSourceMaps(error, trace){
4042

4143
letlastSourceMap;
4244
letlastFileName;
43-
constpreparedTrace=ArrayPrototypeJoin(ArrayPrototypeMap(trace,(t,i)=>{
44-
conststr='\n at ';
45+
constpreparedTrace=ArrayPrototypeJoin(ArrayPrototypeMap(trace,(callSite,i)=>{
4546
try{
4647
// A stack trace will often have several call sites in a row within the
4748
// same file, cache the source map and file content accordingly:
48-
letfileName=t.getFileName();
49+
letfileName=callSite.getFileName();
4950
if(fileName===undefined){
50-
fileName=t.getEvalOrigin();
51+
fileName=callSite.getEvalOrigin();
5152
}
5253
constsm=fileName===lastFileName ?
5354
lastSourceMap :
5455
findSourceMap(fileName);
5556
lastSourceMap=sm;
5657
lastFileName=fileName;
5758
if(sm){
58-
// Source Map V3 lines/columns start at 0/0 whereas stack traces
59-
// start at 1/1:
60-
const{
61-
originalLine,
62-
originalColumn,
63-
originalSource,
64-
}=sm.findEntry(t.getLineNumber()-1,t.getColumnNumber()-1);
65-
if(originalSource&&originalLine!==undefined&&
66-
originalColumn!==undefined){
67-
constname=getOriginalSymbolName(sm,trace,i);
68-
// Construct call site name based on: v8.dev/docs/stack-trace-api:
69-
constfnName=t.getFunctionName()??t.getMethodName();
70-
consttypeName=t.getTypeName();
71-
constnamePrefix=typeName!==null&&typeName!=='global' ? `${typeName}.` : '';
72-
constoriginalName=`${namePrefix}${fnName||'<anonymous>'}`;
73-
// The original call site may have a different symbol name
74-
// associated with it, use it:
75-
constprefix=(name&&name!==originalName) ?
76-
`${name}` :
77-
`${originalName}`;
78-
consthasName=!!(name||originalName);
79-
constoriginalSourceNoScheme=
80-
StringPrototypeStartsWith(originalSource,'file://') ?
81-
fileURLToPath(originalSource) : originalSource;
82-
// Replace the transpiled call site with the original:
83-
return`${str}${prefix}${hasName ? ' (' : ''}`+
84-
`${originalSourceNoScheme}:${originalLine+1}:`+
85-
`${originalColumn+1}${hasName ? ')' : ''}`;
86-
}
59+
return`${kStackLineAt}${serializeJSStackFrame(sm,callSite,trace[i+1])}`;
8760
}
8861
}catch(err){
8962
debug(err);
9063
}
91-
return`${str}${t}`;
64+
return`${kStackLineAt}${callSite}`;
9265
}),'');
9366
return`${errorString}${preparedTrace}`;
9467
}
9568

69+
/**
70+
* Serialize a single call site in the stack trace.
71+
* Refer to SerializeJSStackFrame in deps/v8/src/objects/call-site-info.cc for
72+
* more details about the default ToString(CallSite).
73+
* The CallSite API is documented at https://v8.dev/docs/stack-trace-api.
74+
* @param{import('internal/source_map/source_map').SourceMap} sm
75+
* @param{CallSite} callSite - the CallSite object to be serialized
76+
* @param{CallSite} callerCallSite - caller site info
77+
* @returns{string} - the serialized call site
78+
*/
79+
functionserializeJSStackFrame(sm,callSite,callerCallSite){
80+
// Source Map V3 lines/columns start at 0/0 whereas stack traces
81+
// start at 1/1:
82+
const{
83+
originalLine,
84+
originalColumn,
85+
originalSource,
86+
}=sm.findEntry(callSite.getLineNumber()-1,callSite.getColumnNumber()-1);
87+
if(originalSource===undefined||originalLine===undefined||
88+
originalColumn===undefined){
89+
return`${callSite}`;
90+
}
91+
constname=getOriginalSymbolName(sm,callSite,callerCallSite);
92+
constoriginalSourceNoScheme=
93+
StringPrototypeStartsWith(originalSource,'file://') ?
94+
fileURLToPath(originalSource) : originalSource;
95+
// Construct call site name based on: v8.dev/docs/stack-trace-api:
96+
constfnName=callSite.getFunctionName()??callSite.getMethodName();
97+
98+
letprefix='';
99+
if(callSite.isAsync()){
100+
// Promise aggregation operation frame has no locations. This must be an
101+
// async stack frame.
102+
prefix='async ';
103+
}elseif(callSite.isConstructor()){
104+
prefix='new ';
105+
}
106+
107+
consttypeName=callSite.getTypeName();
108+
constnamePrefix=typeName!==null&&typeName!=='global' ? `${typeName}.` : '';
109+
constoriginalName=`${namePrefix}${fnName||'<anonymous>'}`;
110+
// The original call site may have a different symbol name
111+
// associated with it, use it:
112+
constmappedName=(name&&name!==originalName) ?
113+
`${name}` :
114+
`${originalName}`;
115+
consthasName=!!(name||originalName);
116+
// Replace the transpiled call site with the original:
117+
return`${prefix}${mappedName}${hasName ? ' (' : ''}`+
118+
`${originalSourceNoScheme}:${originalLine+1}:`+
119+
`${originalColumn+1}${hasName ? ')' : ''}`;
120+
}
121+
96122
// Transpilers may have removed the original symbol name used in the stack
97123
// trace, if possible restore it from the names field of the source map:
98-
functiongetOriginalSymbolName(sourceMap,trace,curIndex){
124+
functiongetOriginalSymbolName(sourceMap,callSite,callerCallSite){
99125
// First check for a symbol name associated with the enclosing function:
100126
constenclosingEntry=sourceMap.findEntry(
101-
trace[curIndex].getEnclosingLineNumber()-1,
102-
trace[curIndex].getEnclosingColumnNumber()-1,
127+
callSite.getEnclosingLineNumber()-1,
128+
callSite.getEnclosingColumnNumber()-1,
103129
);
104130
if(enclosingEntry.name)returnenclosingEntry.name;
105-
// Fallback to using the symbol name attached to the next stack frame:
106-
constcurrentFileName=trace[curIndex].getFileName();
107-
constnextCallSite=trace[curIndex+1];
108-
if(nextCallSite&&currentFileName===nextCallSite.getFileName()){
131+
// Fallback to using the symbol name attached to the caller site:
132+
constcurrentFileName=callSite.getFileName();
133+
if(callerCallSite&&currentFileName===callerCallSite.getFileName()){
109134
const{ name }=sourceMap.findEntry(
110-
nextCallSite.getLineNumber()-1,
111-
nextCallSite.getColumnNumber()-1,
135+
callerCallSite.getLineNumber()-1,
136+
callerCallSite.getColumnNumber()-1,
112137
);
113138
returnname;
114139
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Flags: --enable-source-maps
2+
import'../../../common/index.mjs';
3+
asyncfunctionThrow(){
4+
await0;
5+
thrownewError('message');
6+
}
7+
(asyncfunctionmain(){
8+
awaitPromise.all([0,1,2,Throw()]);
9+
})();
10+
// To recreate:
11+
//
12+
// npx --package typescript tsc --module nodenext --target esnext --outDir test/fixtures/source-map/output --sourceMap test/fixtures/source-map/output/source_map_throw_async_stack_trace.mts
13+
//# sourceMappingURL=source_map_throw_async_stack_trace.mjs.map

‎test/fixtures/source-map/output/source_map_throw_async_stack_trace.mjs.map‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Flags: --enable-source-maps
2+
3+
import'../../../common/index.mjs';
4+
5+
interfaceFoo{
6+
/** line
7+
*
8+
* blocks */
9+
}
10+
11+
asyncfunctionThrow(){
12+
await0;
13+
thrownewError('message')
14+
}
15+
16+
(asyncfunctionmain(){
17+
awaitPromise.all([0,1,2,Throw()]);
18+
})()
19+
20+
// To recreate:
21+
//
22+
// npx --package typescript tsc --module nodenext --target esnext --outDir test/fixtures/source-map/output --sourceMap test/fixtures/source-map/output/source_map_throw_async_stack_trace.mts
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
*output*source_map_throw_async_stack_trace.mts:13
2+
throw new Error('message')
3+
^
4+
5+
6+
Error: message
7+
at Throw (*output*source_map_throw_async_stack_trace.mts:13:9)
8+
at async Promise.all (index 3)
9+
at async main (*output*source_map_throw_async_stack_trace.mts:17:3)
10+
11+
Node.js *
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Flags: --enable-source-maps
2+
import'../../../common/index.mjs';
3+
classFoo{
4+
constructor(){
5+
thrownewError('message');
6+
}
7+
}
8+
newFoo();
9+
// To recreate:
10+
//
11+
// npx --package typescript tsc --module nodenext --target esnext --outDir test/fixtures/source-map/output --sourceMap test/fixtures/source-map/output/source_map_throw_construct.mts
12+
//# sourceMappingURL=source_map_throw_construct.mjs.map

‎test/fixtures/source-map/output/source_map_throw_construct.mjs.map‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Flags: --enable-source-maps
2+
3+
import'../../../common/index.mjs';
4+
5+
interfaceBlock{
6+
/** line
7+
*
8+
* blocks */
9+
}
10+
11+
classFoo{
12+
constructor(){
13+
thrownewError('message');
14+
}
15+
}
16+
17+
newFoo();
18+
19+
// To recreate:
20+
//
21+
// npx --package typescript tsc --module nodenext --target esnext --outDir test/fixtures/source-map/output --sourceMap test/fixtures/source-map/output/source_map_throw_construct.mts
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
*output*source_map_throw_construct.mts:13
2+
throw new Error('message');
3+
^
4+
5+
6+
Error: message
7+
at new Foo (*output*source_map_throw_construct.mts:13:11)
8+
at <anonymous> (*output*source_map_throw_construct.mts:17:1)
9+
*
10+
*
11+
*
12+
13+
Node.js *

‎test/fixtures/source-map/output/source_map_throw_set_immediate.snapshot‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@
66
Error: goodbye
77
at Hello (*uglify-throw-original.js:5:9)
88
at Immediate.<anonymous> (*uglify-throw-original.js:9:3)
9-
at process.processImmediate (node:internal*timers:483:21)
9+
*
1010

1111
Node.js *

0 commit comments

Comments
(0)