Skip to content

Commit 0af8489

Browse files
romtsnclaude
andcommitted
feat(r8): Add line span expansion and outside-range fallback
When a base entry maps to a span of original lines (e.g., `:42:44`), expand into one frame per original line. When a frame's line falls outside all mapped ranges, fall back to class-only remapping. Fixes test_different_line_number_span_stacktrace and test_outside_line_range_stacktrace. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 58326db commit 0af8489

File tree

2 files changed

+80
-5
lines changed

2 files changed

+80
-5
lines changed

src/cache/mod.rs

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,8 @@ pub struct RemappedFrameIter<'r, 'data> {
838838
had_mappings: bool,
839839
/// Whether this method has any line-based mappings.
840840
has_line_info: bool,
841+
/// Whether any frame was successfully matched by iterate_with_lines.
842+
matched_any: bool,
841843
/// The source file of the outer class for synthesis.
842844
outer_source_file: Option<&'data str>,
843845
}
@@ -851,6 +853,7 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> {
851853
skip_count: 0,
852854
had_mappings: false,
853855
has_line_info: false,
856+
matched_any: false,
854857
outer_source_file: None,
855858
}
856859
}
@@ -871,6 +874,7 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> {
871874
skip_count,
872875
had_mappings,
873876
has_line_info,
877+
matched_any: false,
874878
outer_source_file,
875879
}
876880
}
@@ -883,6 +887,7 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> {
883887
skip_count: 0,
884888
had_mappings: false,
885889
has_line_info: false,
890+
matched_any: false,
886891
outer_source_file: None,
887892
}
888893
}
@@ -932,9 +937,20 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> {
932937
&mut members,
933938
self.outer_source_file,
934939
self.has_line_info,
940+
&mut self.pending_frames,
935941
);
936-
self.inner = Some((cache, frame, members));
937-
mapped
942+
if mapped.is_some() {
943+
self.matched_any = true;
944+
self.inner = Some((cache, frame, members));
945+
mapped
946+
} else if !self.matched_any && self.has_line_info {
947+
// Outside-range fallback: no member matched the frame line.
948+
// Remap only the class name, keeping the obfuscated method name
949+
// and the original line.
950+
Some(cache.remap_class_only(&frame, self.outer_source_file))
951+
} else {
952+
None
953+
}
938954
} else {
939955
let mapped =
940956
iterate_without_lines(cache, &mut frame, &mut members, self.outer_source_file);
@@ -965,6 +981,7 @@ fn iterate_with_lines<'a>(
965981
members: &mut std::slice::Iter<'_, raw::Member>,
966982
outer_source_file: Option<&str>,
967983
has_line_info: bool,
984+
pending_frames: &mut Vec<StackFrame<'a>>,
968985
) -> Option<StackFrame<'a>> {
969986
let frame_line = frame.line.unwrap_or(0);
970987
for member in members {
@@ -974,10 +991,44 @@ fn iterate_with_lines<'a>(
974991
}
975992
// If the mapping entry has no line range, determine output line.
976993
if member.endline().unwrap_or(0) == 0 {
977-
let output_line = if member.original_startline().is_none() {
994+
if member.original_startline().is_none() {
978995
// Bare method mapping: pass through frame line.
979-
frame.line
980-
} else if member.original_startline().unwrap_or(0) > 0 {
996+
return map_member_without_lines(
997+
cache,
998+
frame,
999+
member,
1000+
outer_source_file,
1001+
frame.line,
1002+
);
1003+
}
1004+
// Span expansion: if the original range spans multiple lines,
1005+
// emit one frame per original line.
1006+
if member.original_endline != u32::MAX
1007+
&& member.original_endline > member.original_startline().unwrap_or(0)
1008+
{
1009+
let first_line = member.original_startline().unwrap_or(0) as usize;
1010+
let last_line = member.original_endline as usize;
1011+
let mut first_frame = None;
1012+
for line in first_line..=last_line {
1013+
if let Some(f) = map_member_without_lines(
1014+
cache,
1015+
frame,
1016+
member,
1017+
outer_source_file,
1018+
Some(line),
1019+
) {
1020+
if first_frame.is_none() {
1021+
first_frame = Some(f);
1022+
} else {
1023+
pending_frames.push(f);
1024+
}
1025+
}
1026+
}
1027+
// Reverse so pop() drains in ascending line order.
1028+
pending_frames.reverse();
1029+
return first_frame;
1030+
}
1031+
let output_line = if member.original_startline().unwrap_or(0) > 0 {
9811032
Some(member.original_startline().unwrap_or(0) as usize)
9821033
} else {
9831034
None

src/mapper.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,22 @@ impl<'s> ProguardMapper<'s> {
699699
collected.rewrite_rules.extend(member.rewrite_rules.iter());
700700
continue;
701701
}
702+
// Span expansion: if the original range spans multiple lines,
703+
// emit one frame per original line.
704+
if let Some(oe) = member.original_endline {
705+
let os = member.original_startline.unwrap_or(0);
706+
if oe > os {
707+
for line in os..=oe {
708+
collected.frames.push(map_member_without_lines(
709+
&frame,
710+
member,
711+
Some(line),
712+
));
713+
}
714+
collected.rewrite_rules.extend(member.rewrite_rules.iter());
715+
continue;
716+
}
717+
}
702718
// Single-line: use original_startline if > 0, else None.
703719
let output_line = if member.original_startline.unwrap_or(0) > 0 {
704720
member.original_startline
@@ -714,6 +730,14 @@ impl<'s> ProguardMapper<'s> {
714730
collected.rewrite_rules.extend(member.rewrite_rules.iter());
715731
}
716732
}
733+
734+
// Outside-range fallback: if we had line mappings but nothing matched,
735+
// remap only the class name, keeping the obfuscated method name and original line.
736+
if collected.frames.is_empty() && has_line_info {
737+
collected
738+
.frames
739+
.push(remap_class_only(&frame, frame.file()));
740+
}
717741
} else {
718742
for member in mapping_entries {
719743
// For parameter-based lookups, use original_startline if > 0, else None

0 commit comments

Comments
 (0)