@@ -30,8 +30,8 @@ struct ClassMapping {
3030struct MethodMapping {
3131 original_name : String ,
3232 obfuscated_name : String ,
33- start_line : u32 ,
34- end_line : u32 ,
33+ start_line : Option < u32 > ,
34+ end_line : Option < u32 > ,
3535}
3636
3737impl ProguardMapping {
@@ -100,11 +100,17 @@ impl ProguardMapping {
100100 }
101101
102102 fn parse_many < ' a > ( inputs : impl IntoIterator < Item = & ' a str > ) -> Result < Self , AppError > {
103- let mut classes = HashMap :: new ( ) ;
103+ let mut classes: HashMap < String , ClassMapping > = HashMap :: new ( ) ;
104104
105105 for input in inputs {
106106 let mapping = Self :: parse ( input) ?;
107- classes. extend ( mapping. classes ) ;
107+ for ( obfuscated_name, class) in mapping. classes {
108+ if let Some ( existing) = classes. get_mut ( & obfuscated_name) {
109+ existing. merge ( class) ;
110+ } else {
111+ classes. insert ( obfuscated_name, class) ;
112+ }
113+ }
108114 }
109115
110116 Ok ( ProguardMapping { classes } )
@@ -214,8 +220,11 @@ impl ProguardMapping {
214220 . iter ( )
215221 . find ( |m| {
216222 m. obfuscated_name == obf_method
217- && line_num >= m. start_line
218- && line_num <= m. end_line
223+ && matches ! (
224+ ( m. start_line, m. end_line) ,
225+ ( Some ( start_line) , Some ( end_line) )
226+ if line_num >= start_line && line_num <= end_line
227+ )
219228 } )
220229 . or_else ( || {
221230 class
@@ -251,6 +260,16 @@ impl ProguardMapping {
251260 }
252261}
253262
263+ impl ClassMapping {
264+ fn merge ( & mut self , other : ClassMapping ) {
265+ if self . file_name . is_none ( ) {
266+ self . file_name = other. file_name ;
267+ }
268+ self . methods . extend ( other. methods ) ;
269+ self . fields . extend ( other. fields ) ;
270+ }
271+ }
272+
254273/// Parse "original.Class -> obfuscated.Class:" into (original, obfuscated)
255274fn parse_class_line ( line : & str ) -> Option < ( String , String ) > {
256275 let line = line. strip_suffix ( ':' ) ?;
@@ -274,11 +293,18 @@ fn parse_member_line(line: &str, class: &mut ClassMapping) {
274293 class. methods . push ( MethodMapping {
275294 original_name : method. name ,
276295 obfuscated_name : obfuscated,
277- start_line : method. start_line ,
278- end_line : method. end_line ,
296+ start_line : Some ( method. start_line ) ,
297+ end_line : Some ( method. end_line ) ,
279298 } ) ;
280299 } else if original_part. contains ( '(' ) {
281- // Method without line numbers — no range info to store
300+ if let Some ( method_name) = parse_method_name ( original_part) {
301+ class. methods . push ( MethodMapping {
302+ original_name : method_name,
303+ obfuscated_name : obfuscated,
304+ start_line : None ,
305+ end_line : None ,
306+ } ) ;
307+ }
282308 } else {
283309 // Field mapping: "type fieldName -> obfuscated"
284310 // We just need the field name (last token before ->)
@@ -295,6 +321,17 @@ struct ParsedMethod {
295321 end_line : u32 ,
296322}
297323
324+ fn parse_method_name ( s : & str ) -> Option < String > {
325+ let s = s. trim ( ) ;
326+ let paren_pos = s. find ( '(' ) ?;
327+ let before_paren = & s[ ..paren_pos] ;
328+ let method_name = before_paren
329+ . rsplit_once ( ' ' )
330+ . map ( |( _, name) | name)
331+ . unwrap_or ( before_paren) ;
332+ Some ( method_name. to_string ( ) )
333+ }
334+
298335fn parse_method_with_lines ( s : & str ) -> Option < ParsedMethod > {
299336 let s = s. trim ( ) ;
300337 // Format: "startLine:endLine:returnType methodName(params)"
@@ -435,10 +472,26 @@ core.file.Validatable -> a.a.b:
435472 . filter ( |m| m. obfuscated_name == "<init>" )
436473 . collect ( ) ;
437474 assert_eq ! ( init_methods. len( ) , 2 ) ;
438- assert_eq ! ( init_methods[ 0 ] . start_line, 29 ) ;
439- assert_eq ! ( init_methods[ 0 ] . end_line, 33 ) ;
440- assert_eq ! ( init_methods[ 1 ] . start_line, 42 ) ;
441- assert_eq ! ( init_methods[ 1 ] . end_line, 43 ) ;
475+ assert_eq ! ( init_methods[ 0 ] . start_line, Some ( 29 ) ) ;
476+ assert_eq ! ( init_methods[ 0 ] . end_line, Some ( 33 ) ) ;
477+ assert_eq ! ( init_methods[ 1 ] . start_line, Some ( 42 ) ) ;
478+ assert_eq ! ( init_methods[ 1 ] . end_line, Some ( 43 ) ) ;
479+ }
480+
481+ #[ test]
482+ fn parse_method_mappings_without_line_numbers ( ) {
483+ let mapping = parse_test_mapping ( SAMPLE_MAPPING ) ;
484+ let file_io = & mapping. classes [ "a.a.a" ] ;
485+
486+ let load_method = file_io
487+ . methods
488+ . iter ( )
489+ . find ( |m| m. obfuscated_name == "b" )
490+ . expect ( "load method should be retained" ) ;
491+
492+ assert_eq ! ( load_method. original_name, "load" ) ;
493+ assert_eq ! ( load_method. start_line, None ) ;
494+ assert_eq ! ( load_method. end_line, None ) ;
442495 }
443496
444497 #[ test]
@@ -528,6 +581,14 @@ java.lang.RuntimeException: oops
528581 assert_eq ! ( output, "\t at core.file.FileIO.reload(FileIO.java)" ) ;
529582 }
530583
584+ #[ test]
585+ fn retrace_unknown_source_uses_method_without_line_numbers ( ) {
586+ let mapping = parse_test_mapping ( SAMPLE_MAPPING ) ;
587+ let input = "\t at a.a.a.b(Unknown Source)" ;
588+ let output = mapping. retrace ( input) ;
589+ assert_eq ! ( output, "\t at core.file.FileIO.load(FileIO.java)" ) ;
590+ }
591+
531592 #[ test]
532593 fn parse_many_combines_split_mapping_files ( ) {
533594 const PART_ONE : & str = r#"core.file.FileIO -> a.a.a:
@@ -548,6 +609,29 @@ java.lang.RuntimeException: oops
548609 ) ;
549610 }
550611
612+ #[ test]
613+ fn parse_many_merges_split_class_mappings ( ) {
614+ const PART_ONE : & str = r#"core.file.FileIO -> a.a.a:
615+ # {"fileName":"FileIO.java","id":"sourceFile"}
616+ 92:92:core.file.FileIO reload() -> c
617+ "# ;
618+ const PART_TWO : & str = r#"core.file.FileIO -> a.a.a:
619+ java.lang.Object load() -> b
620+ "# ;
621+
622+ let mapping = parse_test_mappings ( [ PART_ONE , PART_TWO ] ) ;
623+ let file_io = & mapping. classes [ "a.a.a" ] ;
624+
625+ assert_eq ! ( file_io. file_name. as_deref( ) , Some ( "FileIO.java" ) ) ;
626+ assert ! ( file_io. methods. iter( ) . any( |m| m. obfuscated_name == "c" ) ) ;
627+ assert ! (
628+ file_io
629+ . methods
630+ . iter( )
631+ . any( |m| m. obfuscated_name == "b" && m. original_name == "load" )
632+ ) ;
633+ }
634+
551635 #[ test]
552636 fn retrace_stacktrace_uses_all_mapping_parts ( ) {
553637 const PART_ONE : & str = r#"core.file.FileIO -> a.a.a:
0 commit comments