@@ -43,6 +43,14 @@ export class StackTrace {
4343 }
4444 return undefined ;
4545 }
46+
47+ public findFirstUserFrame ( ) : StackFrame | undefined {
48+ return this . findAndParse ( DEFAULT_USER_CODE_FINDER ) ;
49+ }
50+
51+ public renderJustMyCode ( indent = true ) : string [ ] {
52+ return renderCallStackJustMyCode ( this . frames . map ( f => StackFrame . parse ( f ) ) , indent ) ;
53+ }
4654}
4755
4856export class StackFrame {
@@ -99,32 +107,154 @@ export class StackFrame {
99107 fileName = m ? frame . slice ( 0 , - m [ 0 ] . length ) : frame ;
100108 sourceLocation = m ? m [ 0 ] . slice ( 1 ) : '' ;
101109
102- return {
110+ return new StackFrame ( {
103111 fileName,
104112 functionName,
105113 sourceLocation,
114+ } ) ;
115+ }
116+
117+ /**
118+ * The file name this call frame is in
119+ */
120+ public readonly fileName : string ;
121+
122+ /**
123+ * Name of the function this call frame is in
124+ */
125+ public readonly functionName : string ;
126+
127+ /**
128+ * The line and optionally column number this call frame is in
129+ *
130+ * Formatted as `<line> [':' <column>]`.
131+ */
132+ public readonly sourceLocation : string ;
133+
134+ constructor ( props : {
135+ fileName : string ;
136+ functionName : string ;
137+ sourceLocation : string ;
138+ } ) {
139+ this . fileName = props . fileName ;
140+ this . functionName = props . functionName ;
141+ this . sourceLocation = props . sourceLocation ;
142+ }
143+
144+ public get lineLocation ( ) {
145+ if ( ! this . sourceLocation ) {
146+ return undefined ;
147+ }
148+ const parts = this . sourceLocation . split ( ':' ) ;
149+ const line = parseInt ( parts [ 0 ] , 10 ) ;
150+ const column = parseInt ( parts [ 1 ] , 10 ) ;
151+ return {
152+ line : isNaN ( line ) ? undefined : line ,
153+ column : isNaN ( column ) ? undefined : column ,
106154 } ;
107155 }
108156
109- constructor (
110- /**
111- * Name of the function this call frame is in
112- */
113- public readonly functionName : string ,
114-
115- /**
116- * The file name this call frame is in
117- */
118- public readonly fileName : string ,
119-
120- /**
121- * The line and optionally column number this call frame is in
122- *
123- * Formatted as `<line> [':' <column>]`.
124- */
125- public readonly sourceLocation : string ,
126- ) {
157+ public get lineNumber ( ) : number | undefined {
158+ return this . lineLocation ?. line ;
159+ }
160+
161+ public get columnNumber ( ) : number | undefined {
162+ return this . lineLocation ?. column ;
163+ }
164+ }
165+
166+ // Look for `/node_modules/` followed by either
167+ // - An @ sign, and 2 path segments
168+ // - No @ sign, and 1 path segment
169+ const MODULE_RE = / ( \/ | \\ ) n o d e _ m o d u l e s ( \/ | \\ ) ( @ [ ^ / \\ ] + [ / \\ ] [ ^ / \\ ] + | [ ^ @ ] [ ^ / \\ ] * ) / ;
170+
171+ const DECORATOR_RE = / ( \/ | \\ ) ( p r o p - i n j e c t a b l e | n o - b o x - s t a c k - t r a c e s ) \. / ;
172+
173+ /**
174+ * Renders an array of CallSites nicely, focusing on the user application code
175+ *
176+ * We detect "Not My Code" using the following heuristics:
177+ *
178+ * - If there is '/node_modules/' in the file path, we assume the call stack is a library and we skip it.
179+ * - If there is 'node:' in the file path, we assume it is NodeJS internals and we skip it.
180+ * - We skip (and hide) 'prop-injectable' or 'no-box-stack-traces' in the trace, because those are
181+ * constructor decorators that hide what's interesting (and look weird).
182+ *
183+ * If `indent` is enabled, we will prefix stack frames of actual files with " at ", just like
184+ * Node does for its stack frames.
185+ */
186+ function renderCallStackJustMyCode ( stack : StackFrame [ ] , indent = true ) : string [ ] {
187+ const lines = [ ] ;
188+ let skipped = new Array < { functionName ?: string ; fileName : string } > ( ) ;
189+
190+ let sawMyCode = false ;
191+ let i = 0 ;
192+ while ( i < stack . length ) {
193+ const frame = stack [ i ++ ] ;
194+
195+ if ( frame . fileName . match ( DECORATOR_RE ) ) {
196+ continue ;
197+ }
198+
199+ const pat = frame . fileName . match ( MODULE_RE ) ;
200+ if ( pat ) {
201+ while ( i < stack . length && stack [ i ] . fileName . includes ( pat [ 0 ] ) ) {
202+ i ++ ;
203+ }
204+ // The last stack frame has the function that user code call into.
205+ skip ( { functionName : stack [ i - 1 ] . functionName , fileName : pat [ 3 ] } ) ;
206+ } else if ( frame . fileName . includes ( 'node:' ) ) {
207+ skip ( { fileName : 'node internals' } ) ;
208+ while ( i < stack . length && stack [ i ] . fileName . includes ( 'node:' ) ) {
209+ i ++ ;
210+ }
211+ } else if ( isHostInternalFrame ( frame ) ) {
212+ skip ( { fileName : 'jsii runtime' } ) ;
213+ while ( i < stack . length && isHostInternalFrame ( stack [ i ] ) ) {
214+ i ++ ;
215+ }
216+ } else {
217+ reportSkipped ( true ) ;
218+ const prefix = indent ? ' at ' : '' ;
219+ lines . push ( `${ prefix } ${ frame . functionName } (${ frame . fileName } :${ frame . sourceLocation } )` ) ;
220+ sawMyCode = true ;
221+ }
222+ }
223+ reportSkipped ( false ) ;
224+
225+ if ( ! sawMyCode ) {
226+ lines . push ( `${ indent ? ' ' : '' } (no user code in ${ Error . stackTraceLimit } frames, use --stack-trace-limit to capture more)` ) ;
227+ }
228+
229+ return lines ;
230+
231+ function skip ( what : typeof skipped [ number ] ) {
232+ if ( ! skipped . find ( x => x . fileName === what . fileName && x . functionName === what . functionName ) ) {
233+ skipped . push ( what ) ;
234+ }
235+ }
236+
237+ function reportSkipped ( includeFunction : boolean ) {
238+ if ( skipped . length > 0 ) {
239+ const rendered = skipped . map ( ( s , j ) =>
240+ // Only render the function name of the last frame before user code
241+ j === skipped . length - 1 && s . functionName && includeFunction ?
242+ `${ s . functionName } in ${ s . fileName } ` : s . fileName ) ;
127243
244+ const prefix = indent ? ' ' : '' ;
245+ lines . push ( `${ prefix } ...${ rendered . join ( ', ' ) } ...` ) ;
246+ }
247+ skipped = [ ] ;
128248 }
129249}
130250
251+ const JSII_HOST_DIR_NAME_SYM = Symbol . for ( 'jsii.context.hostDirName' ) ;
252+
253+ /**
254+ * Whether the call site comes from the internals of the host that sent us the stack trace
255+ */
256+ function isHostInternalFrame ( frame : StackFrame ) : boolean {
257+ const hostDirName = ( global as any ) [ JSII_HOST_DIR_NAME_SYM ] ;
258+ return hostDirName && frame . fileName . includes ( hostDirName ) ;
259+ }
260+
0 commit comments