Skip to content

Commit b9b4841

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 ecdc202 commit b9b4841

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
@@ -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,44 @@ 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)
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+
// Reverse so pop() drains in ascending line order.
1029+
pending_frames.reverse();
1030+
return first_frame;
1031+
}
1032+
let output_line = if member.original_startline().unwrap_or(0) > 0 {
9821033
Some(member.original_startline().unwrap_or(0) as usize)
9831034
} else {
9841035
None

src/mapper.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,22 @@ impl<'s> ProguardMapper<'s> {
705705
collected.rewrite_rules.extend(member.rewrite_rules.iter());
706706
continue;
707707
}
708+
// Span expansion: if the original range spans multiple lines,
709+
// emit one frame per original line.
710+
if let Some(oe) = member.original_endline {
711+
let os = member.original_startline.unwrap_or(0);
712+
if oe > os {
713+
for line in os..=oe {
714+
collected.frames.push(map_member_without_lines(
715+
&frame,
716+
member,
717+
Some(line),
718+
));
719+
}
720+
collected.rewrite_rules.extend(member.rewrite_rules.iter());
721+
continue;
722+
}
723+
}
708724
// Single-line: use original_startline if > 0, else None.
709725
let output_line = if member.original_startline.unwrap_or(0) > 0 {
710726
member.original_startline
@@ -720,6 +736,14 @@ impl<'s> ProguardMapper<'s> {
720736
collected.rewrite_rules.extend(member.rewrite_rules.iter());
721737
}
722738
}
739+
740+
// Outside-range fallback: if we had line mappings but nothing matched,
741+
// remap only the class name, keeping the obfuscated method name and original line.
742+
if collected.frames.is_empty() && has_line_info {
743+
collected
744+
.frames
745+
.push(remap_class_only(&frame, frame.file()));
746+
}
723747
} else {
724748
for member in mapping_entries {
725749
// For parameter-based lookups, use original_startline if > 0, else None

0 commit comments

Comments
 (0)