Skip to content

Commit 92b293d

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 28bfece commit 92b293d

File tree

2 files changed

+82
-9
lines changed

2 files changed

+82
-9
lines changed

src/cache/mod.rs

Lines changed: 59 additions & 9 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,14 +991,47 @@ fn iterate_with_lines<'a>(
974991
}
975992
// If the mapping entry has no line range, determine output line.
976993
if member.endline == 0 {
977-
let output_line = if !member.has_line_mapping() {
994+
if !member.has_line_mapping() {
978995
// Bare method mapping: pass through frame line.
979-
frame.line
980-
} else if member.original_startline > 0 && member.original_startline != u32::MAX {
981-
Some(member.original_startline as usize)
982-
} else {
983-
None
984-
};
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
1008+
{
1009+
let first_line = member.original_startline 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+
return first_frame;
1028+
}
1029+
let output_line =
1030+
if member.original_startline > 0 && member.original_startline != u32::MAX {
1031+
Some(member.original_startline as usize)
1032+
} else {
1033+
None
1034+
};
9851035
return map_member_without_lines(cache, frame, member, outer_source_file, output_line);
9861036
}
9871037
// skip any members which do not match our frames line

src/mapper.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,21 @@ impl<'s> ProguardMapper<'s> {
707707
collected.rewrite_rules.extend(member.rewrite_rules.iter());
708708
continue;
709709
}
710+
// Span expansion: if the original range spans multiple lines,
711+
// emit one frame per original line.
712+
if let Some(oe) = member.original_endline {
713+
if oe > member.original_startline {
714+
for line in member.original_startline..=oe {
715+
collected.frames.push(map_member_without_lines(
716+
&frame,
717+
member,
718+
Some(line),
719+
));
720+
}
721+
collected.rewrite_rules.extend(member.rewrite_rules.iter());
722+
continue;
723+
}
724+
}
710725
// Single-line: use original_startline if > 0, else None.
711726
let output_line = if member.original_startline > 0 {
712727
Some(member.original_startline)
@@ -722,6 +737,14 @@ impl<'s> ProguardMapper<'s> {
722737
collected.rewrite_rules.extend(member.rewrite_rules.iter());
723738
}
724739
}
740+
741+
// Outside-range fallback: if we had line mappings but nothing matched,
742+
// remap only the class name, keeping the obfuscated method name and original line.
743+
if collected.frames.is_empty() && has_line_info {
744+
collected
745+
.frames
746+
.push(remap_class_only(&frame, frame.file()));
747+
}
725748
} else {
726749
for member in mapping_entries {
727750
// For parameter-based lookups, use original_startline if > 0, else None

0 commit comments

Comments
 (0)