Skip to content

Commit 86ca9fb

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 bc97cdb commit 86ca9fb

File tree

2 files changed

+78
-5
lines changed

2 files changed

+78
-5
lines changed

src/cache/mod.rs

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,8 @@ pub struct RemappedFrameIter<'r, 'data> {
839839
had_mappings: bool,
840840
/// Whether this method has any line-based mappings.
841841
has_line_info: bool,
842+
/// Whether any frame was successfully matched by iterate_with_lines.
843+
matched_any: bool,
842844
/// The source file of the outer class for synthesis.
843845
outer_source_file: Option<&'data str>,
844846
}
@@ -852,6 +854,7 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> {
852854
skip_count: 0,
853855
had_mappings: false,
854856
has_line_info: false,
857+
matched_any: false,
855858
outer_source_file: None,
856859
}
857860
}
@@ -872,6 +875,7 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> {
872875
skip_count,
873876
had_mappings,
874877
has_line_info,
878+
matched_any: false,
875879
outer_source_file,
876880
}
877881
}
@@ -884,6 +888,7 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> {
884888
skip_count: 0,
885889
had_mappings: false,
886890
has_line_info: false,
891+
matched_any: false,
887892
outer_source_file: None,
888893
}
889894
}
@@ -933,9 +938,20 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> {
933938
&mut members,
934939
self.outer_source_file,
935940
self.has_line_info,
941+
&mut self.pending_frames,
936942
);
937-
self.inner = Some((cache, frame, members));
938-
mapped
943+
if mapped.is_some() {
944+
self.matched_any = true;
945+
self.inner = Some((cache, frame, members));
946+
mapped
947+
} else if !self.matched_any && self.has_line_info {
948+
// Outside-range fallback: no member matched the frame line.
949+
// Remap only the class name, keeping the obfuscated method name
950+
// and the original line.
951+
Some(cache.remap_class_only(&frame, self.outer_source_file))
952+
} else {
953+
None
954+
}
939955
} else {
940956
let mapped =
941957
iterate_without_lines(cache, &mut frame, &mut members, self.outer_source_file);
@@ -966,6 +982,7 @@ fn iterate_with_lines<'a>(
966982
members: &mut std::slice::Iter<'_, raw::Member>,
967983
outer_source_file: Option<&str>,
968984
has_line_info: bool,
985+
pending_frames: &mut Vec<StackFrame<'a>>,
969986
) -> Option<StackFrame<'a>> {
970987
let frame_line = frame.line.unwrap_or(0);
971988
for member in members {
@@ -975,10 +992,42 @@ fn iterate_with_lines<'a>(
975992
}
976993
// If the mapping entry has no line range, determine output line.
977994
if member.endline().unwrap_or(0) == 0 {
978-
let output_line = if member.original_startline().is_none() {
995+
if member.original_startline().is_none() {
979996
// Bare method mapping: pass through frame line.
980-
frame.line
981-
} else if member.original_startline().unwrap_or(0) > 0 {
997+
return map_member_without_lines(
998+
cache,
999+
frame,
1000+
member,
1001+
outer_source_file,
1002+
frame.line,
1003+
);
1004+
}
1005+
// Span expansion: if the original range spans multiple lines,
1006+
// emit one frame per original line.
1007+
if member.original_endline != u32::MAX
1008+
&& member.original_endline > member.original_startline().unwrap_or(0) as u32
1009+
{
1010+
let first_line = member.original_startline().unwrap_or(0) as usize;
1011+
let last_line = member.original_endline as usize;
1012+
let mut first_frame = None;
1013+
for line in first_line..=last_line {
1014+
if let Some(f) = map_member_without_lines(
1015+
cache,
1016+
frame,
1017+
member,
1018+
outer_source_file,
1019+
Some(line),
1020+
) {
1021+
if first_frame.is_none() {
1022+
first_frame = Some(f);
1023+
} else {
1024+
pending_frames.push(f);
1025+
}
1026+
}
1027+
}
1028+
return first_frame;
1029+
}
1030+
let output_line = if member.original_startline().unwrap_or(0) > 0 {
9821031
Some(member.original_startline().unwrap_or(0) as usize)
9831032
} else {
9841033
None

src/mapper.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,22 @@ impl<'s> ProguardMapper<'s> {
696696
collected.rewrite_rules.extend(member.rewrite_rules.iter());
697697
continue;
698698
}
699+
// Span expansion: if the original range spans multiple lines,
700+
// emit one frame per original line.
701+
if let Some(oe) = member.original_endline {
702+
let os = member.original_startline.unwrap_or(0);
703+
if oe > os {
704+
for line in os..=oe {
705+
collected.frames.push(map_member_without_lines(
706+
&frame,
707+
member,
708+
Some(line),
709+
));
710+
}
711+
collected.rewrite_rules.extend(member.rewrite_rules.iter());
712+
continue;
713+
}
714+
}
699715
// Single-line: use original_startline if > 0, else None.
700716
let output_line = if member.original_startline.unwrap_or(0) > 0 {
701717
member.original_startline
@@ -711,6 +727,14 @@ impl<'s> ProguardMapper<'s> {
711727
collected.rewrite_rules.extend(member.rewrite_rules.iter());
712728
}
713729
}
730+
731+
// Outside-range fallback: if we had line mappings but nothing matched,
732+
// remap only the class name, keeping the obfuscated method name and original line.
733+
if collected.frames.is_empty() && has_line_info {
734+
collected
735+
.frames
736+
.push(remap_class_only(&frame, frame.file()));
737+
}
714738
} else {
715739
for member in mapping_entries {
716740
// For parameter-based lookups, use original_startline if > 0, else None

0 commit comments

Comments
 (0)