1717use std:: collections:: HashMap ;
1818use std:: fmt;
1919
20+ use srcmap_scopes:: GeneratedRange ;
2021use srcmap_sourcemap:: SourceMap ;
2122
2223// ── Types ───────────────────────────────────────────────────────
@@ -37,7 +38,7 @@ pub struct StackFrame {
3738/// A symbolicated (resolved) stack frame.
3839#[ derive( Debug , Clone , PartialEq , Eq ) ]
3940pub struct SymbolicatedFrame {
40- /// Original function name from source map ( or original function name) .
41+ /// Original function name from source map mappings or scopes data .
4142 pub function_name : Option < String > ,
4243 /// Resolved original source file.
4344 pub file : String ,
@@ -267,6 +268,7 @@ where
267268 function_name : loc
268269 . name
269270 . map ( |n| sm. name ( n) . to_string ( ) )
271+ . or_else ( || find_original_function_name ( sm, line, column) )
270272 . or_else ( || frame. function_name . clone ( ) ) ,
271273 file : sm. source ( loc. source ) . to_string ( ) ,
272274 line : loc. line + 1 , // back to 1-based
@@ -297,6 +299,59 @@ where
297299 SymbolicatedStack { message, frames : result_frames }
298300}
299301
302+ fn find_original_function_name ( sm : & SourceMap , line : u32 , column : u32 ) -> Option < String > {
303+ let scopes = sm. scopes . as_ref ( ) ?;
304+ let mut path = Vec :: new ( ) ;
305+ if !collect_innermost_range_path ( & scopes. ranges , line, column, & mut path) {
306+ return None ;
307+ }
308+
309+ for range in path. iter ( ) . rev ( ) {
310+ if !range. is_stack_frame || range. is_hidden {
311+ continue ;
312+ }
313+
314+ let definition = match range. definition {
315+ Some ( definition) => definition,
316+ None => continue ,
317+ } ;
318+ let scope = scopes. original_scope_for_definition ( definition) ?;
319+ if let Some ( name) = & scope. name {
320+ return Some ( name. clone ( ) ) ;
321+ }
322+ }
323+
324+ None
325+ }
326+
327+ fn collect_innermost_range_path < ' a > (
328+ ranges : & ' a [ GeneratedRange ] ,
329+ line : u32 ,
330+ column : u32 ,
331+ path : & mut Vec < & ' a GeneratedRange > ,
332+ ) -> bool {
333+ for range in ranges {
334+ if !range_contains_position ( range, line, column) {
335+ continue ;
336+ }
337+
338+ path. push ( range) ;
339+ if collect_innermost_range_path ( & range. children , line, column, path) {
340+ return true ;
341+ }
342+ return true ;
343+ }
344+
345+ false
346+ }
347+
348+ fn range_contains_position ( range : & GeneratedRange , line : u32 , column : u32 ) -> bool {
349+ let pos = ( line, column) ;
350+ let start = ( range. start . line , range. start . column ) ;
351+ let end = ( range. end . line , range. end . column ) ;
352+ start <= pos && pos <= end
353+ }
354+
300355/// Batch symbolicate multiple stack traces against pre-loaded source maps.
301356///
302357/// `maps` is a map of source file → SourceMap. All stack traces are resolved
@@ -348,6 +403,7 @@ pub fn to_json(stack: &SymbolicatedStack) -> String {
348403#[ cfg( test) ]
349404mod tests {
350405 use super :: * ;
406+ use srcmap_scopes:: { GeneratedRange , OriginalScope , Position , ScopeInfo } ;
351407
352408 // ── V8 format tests ──────────────────────────────────────────
353409
@@ -426,6 +482,97 @@ mod tests {
426482 assert_eq ! ( result. frames[ 0 ] . function_name. as_deref( ) , Some ( "handleClick" ) ) ;
427483 }
428484
485+ #[ test]
486+ fn symbolicate_uses_scopes_function_name_when_mapping_name_is_absent ( ) {
487+ let scope_info = ScopeInfo {
488+ scopes : vec ! [ Some ( OriginalScope {
489+ start: Position { line: 10 , column: 0 } ,
490+ end: Position { line: 20 , column: 0 } ,
491+ name: Some ( "originalFunc" . to_string( ) ) ,
492+ kind: Some ( "function" . to_string( ) ) ,
493+ is_stack_frame: true ,
494+ variables: vec![ ] ,
495+ children: vec![ ] ,
496+ } ) ] ,
497+ ranges : vec ! [ GeneratedRange {
498+ start: Position { line: 0 , column: 0 } ,
499+ end: Position { line: 0 , column: 10 } ,
500+ is_stack_frame: true ,
501+ is_hidden: false ,
502+ definition: Some ( 0 ) ,
503+ call_site: None ,
504+ bindings: vec![ ] ,
505+ children: vec![ ] ,
506+ } ] ,
507+ } ;
508+ let mut names = vec ! [ ] ;
509+ let scopes_str = srcmap_scopes:: encode_scopes ( & scope_info, & mut names) ;
510+ let names_json = serde_json:: to_string ( & names) . unwrap ( ) ;
511+ let map_json = format ! (
512+ r#"{{"version":3,"sources":["src/app.ts"],"names":{names_json},"mappings":"AAAA","scopes":"{scopes_str}"}}"#
513+ ) ;
514+
515+ let result = symbolicate ( "Error: test\n at foo (bundle.js:1:1)" , |file| {
516+ if file == "bundle.js" { SourceMap :: from_json ( & map_json) . ok ( ) } else { None }
517+ } ) ;
518+
519+ assert_eq ! ( result. frames[ 0 ] . function_name. as_deref( ) , Some ( "originalFunc" ) ) ;
520+ }
521+
522+ #[ test]
523+ fn symbolicate_skips_hidden_scopes_and_uses_outer_stack_frame_name ( ) {
524+ let scope_info = ScopeInfo {
525+ scopes : vec ! [ Some ( OriginalScope {
526+ start: Position { line: 0 , column: 0 } ,
527+ end: Position { line: 30 , column: 0 } ,
528+ name: Some ( "outerFunc" . to_string( ) ) ,
529+ kind: Some ( "function" . to_string( ) ) ,
530+ is_stack_frame: true ,
531+ variables: vec![ ] ,
532+ children: vec![ OriginalScope {
533+ start: Position { line: 5 , column: 0 } ,
534+ end: Position { line: 10 , column: 0 } ,
535+ name: Some ( "hiddenInner" . to_string( ) ) ,
536+ kind: Some ( "function" . to_string( ) ) ,
537+ is_stack_frame: true ,
538+ variables: vec![ ] ,
539+ children: vec![ ] ,
540+ } ] ,
541+ } ) ] ,
542+ ranges : vec ! [ GeneratedRange {
543+ start: Position { line: 0 , column: 0 } ,
544+ end: Position { line: 0 , column: 20 } ,
545+ is_stack_frame: true ,
546+ is_hidden: false ,
547+ definition: Some ( 0 ) ,
548+ call_site: None ,
549+ bindings: vec![ ] ,
550+ children: vec![ GeneratedRange {
551+ start: Position { line: 0 , column: 5 } ,
552+ end: Position { line: 0 , column: 10 } ,
553+ is_stack_frame: true ,
554+ is_hidden: true ,
555+ definition: Some ( 1 ) ,
556+ call_site: None ,
557+ bindings: vec![ ] ,
558+ children: vec![ ] ,
559+ } ] ,
560+ } ] ,
561+ } ;
562+ let mut names = vec ! [ ] ;
563+ let scopes_str = srcmap_scopes:: encode_scopes ( & scope_info, & mut names) ;
564+ let names_json = serde_json:: to_string ( & names) . unwrap ( ) ;
565+ let map_json = format ! (
566+ r#"{{"version":3,"sources":["src/app.ts"],"names":{names_json},"mappings":"AAAA","scopes":"{scopes_str}"}}"#
567+ ) ;
568+
569+ let result = symbolicate ( "Error: test\n at foo (bundle.js:1:7)" , |file| {
570+ if file == "bundle.js" { SourceMap :: from_json ( & map_json) . ok ( ) } else { None }
571+ } ) ;
572+
573+ assert_eq ! ( result. frames[ 0 ] . function_name. as_deref( ) , Some ( "outerFunc" ) ) ;
574+ }
575+
429576 #[ test]
430577 fn symbolicate_no_map ( ) {
431578 let stack = "Error: test\n at foo (unknown.js:10:5)" ;
0 commit comments