Skip to content

Commit 74783ef

Browse files
romtsnclaude
andcommitted
refactor(stacktrace): Change StackFrame.line from usize to Option<usize>
Distinguish "no line info" (None) from "line 0" (Some(0)) in stack frames. This is a prerequisite for correctly handling R8's various no-line mapping semantics where the absence of line information has different behavior than an explicit line 0. - StackFrame::new/with_file wrap line in Some() - StackFrame::with_parameters uses None instead of 0 - parse_frame returns None for missing line, Some(n) for present line - Display omits `:line` suffix when line is None - All call sites updated with unwrap_or(0) to preserve current behavior - All test assertions updated to use Some(N) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6bb30fc commit 74783ef

File tree

5 files changed

+97
-92
lines changed

5 files changed

+97
-92
lines changed

src/cache/mod.rs

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ impl<'data> ProguardCache<'data> {
168168
class: frame.class,
169169
method: frame.method,
170170
file,
171-
line: frame.line,
171+
line: Some(frame.line.unwrap_or(0)),
172172
parameters: frame.parameters,
173173
method_synthesized: false,
174174
}
@@ -389,9 +389,10 @@ impl<'data> ProguardCache<'data> {
389389
if prepared_frame.parameters.is_none() {
390390
for member in mapping_entries {
391391
// Check if this member would produce a frame (line matching)
392+
let pf_line = prepared_frame.line.unwrap_or(0);
392393
if member.endline == 0
393-
|| (prepared_frame.line >= member.startline as usize
394-
&& prepared_frame.line <= member.endline as usize)
394+
|| (pf_line >= member.startline as usize
395+
&& pf_line <= member.endline as usize)
395396
{
396397
had_mappings = true;
397398
rewrite_rules.extend(self.decode_rewrite_rules(member));
@@ -477,7 +478,7 @@ impl<'data> ProguardCache<'data> {
477478
'r: 'data,
478479
{
479480
if self.is_outline_frame(frame.class, frame.method) {
480-
*carried_outline_pos = Some(frame.line);
481+
*carried_outline_pos = Some(frame.line.unwrap_or(0));
481482
return None;
482483
}
483484

@@ -678,11 +679,11 @@ impl<'data> ProguardCache<'data> {
678679
if let Some(mapped) = self.map_outline_position(
679680
effective.class,
680681
effective.method,
681-
effective.line,
682+
effective.line.unwrap_or(0),
682683
pos,
683684
effective.parameters,
684685
) {
685-
effective.line = mapped;
686+
effective.line = Some(mapped);
686687
}
687688
}
688689

@@ -903,7 +904,7 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> {
903904
let out = if frame.parameters.is_none() {
904905
// If we have no line number, treat it as unknown. If there are base (no-line) mappings
905906
// present, prefer those over line-mapped entries.
906-
if frame.line == 0 {
907+
if frame.line.unwrap_or(0) == 0 {
907908
let selection = select_no_line_members(members.as_slice())?;
908909
let mapped = match selection {
909910
NoLineSelection::Single(member) => {
@@ -983,9 +984,10 @@ fn iterate_with_lines<'a>(
983984
outer_source_file: Option<&str>,
984985
has_line_info: bool,
985986
) -> Option<StackFrame<'a>> {
987+
let frame_line = frame.line.unwrap_or(0);
986988
for member in members {
987989
// If this method has line mappings, skip base (no-line) entries when we have a concrete line.
988-
if has_line_info && frame.line > 0 && member.endline == 0 {
990+
if has_line_info && frame_line > 0 && member.endline == 0 {
989991
continue;
990992
}
991993
// If the mapping entry has no line range, preserve the input line number (if any).
@@ -994,7 +996,7 @@ fn iterate_with_lines<'a>(
994996
}
995997
// skip any members which do not match our frames line
996998
if member.endline > 0
997-
&& (frame.line < member.startline as usize || frame.line > member.endline as usize)
999+
&& (frame_line < member.startline as usize || frame_line > member.endline as usize)
9981000
{
9991001
continue;
10001002
}
@@ -1005,7 +1007,7 @@ fn iterate_with_lines<'a>(
10051007
{
10061008
member.original_startline as usize
10071009
} else {
1008-
member.original_startline as usize + frame.line - member.startline as usize
1010+
member.original_startline as usize + frame_line - member.startline as usize
10091011
};
10101012

10111013
let class = cache
@@ -1035,7 +1037,7 @@ fn iterate_with_lines<'a>(
10351037
class,
10361038
method,
10371039
file,
1038-
line,
1040+
line: Some(line),
10391041
parameters: frame.parameters,
10401042
method_synthesized: member.is_synthesized(),
10411043
});
@@ -1105,12 +1107,12 @@ fn map_member_without_lines<'a>(
11051107
class,
11061108
method,
11071109
file,
1108-
line: resolve_no_line_output_line(
1109-
frame.line,
1110+
line: Some(resolve_no_line_output_line(
1111+
frame.line.unwrap_or(0),
11101112
original_startline,
11111113
member.startline as usize,
11121114
member.endline as usize,
1113-
),
1115+
)),
11141116
parameters: frame.parameters,
11151117
method_synthesized: member.is_synthesized(),
11161118
})
@@ -1142,12 +1144,12 @@ fn iterate_without_lines<'a>(
11421144
class,
11431145
method,
11441146
file,
1145-
line: resolve_no_line_output_line(
1146-
frame.line,
1147+
line: Some(resolve_no_line_output_line(
1148+
frame.line.unwrap_or(0),
11471149
original_startline,
11481150
member.startline as usize,
11491151
member.endline as usize,
1150-
),
1152+
)),
11511153
parameters: frame.parameters,
11521154
method_synthesized: member.is_synthesized(),
11531155
})
@@ -1207,15 +1209,15 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g:
12071209
StackFrame {
12081210
class: "com.example.MainFragment$g",
12091211
method: "onClick",
1210-
line: 2,
1212+
line: Some(2),
12111213
file: Some(Cow::Borrowed("SourceFile")),
12121214
parameters: None,
12131215
method_synthesized: false,
12141216
},
12151217
StackFrame {
12161218
class: "android.view.View",
12171219
method: "performClick",
1218-
line: 7393,
1220+
line: Some(7393),
12191221
file: Some(Cow::Borrowed("View.java")),
12201222
parameters: None,
12211223
method_synthesized: false,
@@ -1229,7 +1231,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g:
12291231
frames: vec![StackFrame {
12301232
class: "com.example.MainFragment$g",
12311233
method: "onClick",
1232-
line: 1,
1234+
line: Some(1),
12331235
file: Some(Cow::Borrowed("SourceFile")),
12341236
parameters: None,
12351237
method_synthesized: false,
@@ -1421,15 +1423,15 @@ some.Other -> b:
14211423
StackFrame {
14221424
class: "a",
14231425
method: "call",
1424-
line: 4,
1426+
line: Some(4),
14251427
file: Some(Cow::Borrowed("SourceFile")),
14261428
parameters: None,
14271429
method_synthesized: false,
14281430
},
14291431
StackFrame {
14301432
class: "b",
14311433
method: "run",
1432-
line: 5,
1434+
line: Some(5),
14331435
file: Some(Cow::Borrowed("SourceFile")),
14341436
parameters: None,
14351437
method_synthesized: false,
@@ -1445,6 +1447,6 @@ some.Other -> b:
14451447
assert_eq!(remapped.frames.len(), 1);
14461448
assert_eq!(remapped.frames[0].class, "some.Other");
14471449
assert_eq!(remapped.frames[0].method, "method");
1448-
assert_eq!(remapped.frames[0].line, 30);
1450+
assert_eq!(remapped.frames[0].line, Some(30));
14491451
}
14501452
}

src/mapper.rs

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,8 @@ fn map_member_with_lines<'a>(
140140
frame: &StackFrame<'a>,
141141
member: &MemberMapping<'a>,
142142
) -> Option<StackFrame<'a>> {
143-
if member.endline > 0 && (frame.line < member.startline || frame.line > member.endline) {
143+
let frame_line = frame.line.unwrap_or(0);
144+
if member.endline > 0 && (frame_line < member.startline || frame_line > member.endline) {
144145
return None;
145146
}
146147

@@ -151,7 +152,7 @@ fn map_member_with_lines<'a>(
151152
{
152153
member.original_startline
153154
} else {
154-
member.original_startline + frame.line - member.startline
155+
member.original_startline + frame_line - member.startline
155156
};
156157

157158
let class = member.original_class.unwrap_or(frame.class);
@@ -172,7 +173,7 @@ fn map_member_with_lines<'a>(
172173
class,
173174
method: member.original,
174175
file,
175-
line,
176+
line: Some(line),
176177
parameters: frame.parameters,
177178
method_synthesized: member.is_synthesized,
178179
})
@@ -191,7 +192,7 @@ fn map_member_without_lines<'a>(
191192
None
192193
};
193194
let line = resolve_no_line_output_line(
194-
frame.line,
195+
frame.line.unwrap_or(0),
195196
original_startline,
196197
member.startline,
197198
member.endline,
@@ -202,7 +203,7 @@ fn map_member_without_lines<'a>(
202203
file,
203204
// Preserve input line if present (e.g. "Unknown Source:7") when the mapping itself
204205
// has no line information. This matches R8 retrace behavior.
205-
line,
206+
line: Some(line),
206207
parameters: frame.parameters,
207208
method_synthesized: member.is_synthesized,
208209
}
@@ -214,7 +215,7 @@ fn remap_class_only<'a>(frame: &StackFrame<'a>, reference_file: Option<&str>) ->
214215
class: frame.class,
215216
method: frame.method,
216217
file,
217-
line: frame.line,
218+
line: Some(frame.line.unwrap_or(0)),
218219
parameters: frame.parameters,
219220
method_synthesized: false,
220221
}
@@ -295,9 +296,10 @@ fn iterate_with_lines<'a>(
295296
members: &mut core::slice::Iter<'_, MemberMapping<'a>>,
296297
has_line_info: bool,
297298
) -> Option<StackFrame<'a>> {
299+
let frame_line = frame.line.unwrap_or(0);
298300
for member in members {
299301
// If this method has line mappings, skip base (no-line) entries when we have a concrete line.
300-
if has_line_info && frame.line > 0 && member.endline == 0 {
302+
if has_line_info && frame_line > 0 && member.endline == 0 {
301303
continue;
302304
}
303305
// If the mapping entry has no line range, preserve the input line number (if any).
@@ -522,11 +524,11 @@ impl<'s> ProguardMapper<'s> {
522524
if let Some(mapped) = self.map_outline_position(
523525
effective.class,
524526
effective.method,
525-
effective.line,
527+
effective.line.unwrap_or(0),
526528
pos,
527529
effective.parameters,
528530
) {
529-
effective.line = mapped;
531+
effective.line = Some(mapped);
530532
}
531533
}
532534

@@ -584,7 +586,7 @@ impl<'s> ProguardMapper<'s> {
584586

585587
// If the stacktrace has no line number, treat it as unknown and remap without
586588
// applying line filters. If there are base (no-line) mappings present, prefer those.
587-
if frame.line == 0 {
589+
if frame.line.unwrap_or(0) == 0 {
588590
let selection = select_no_line_members(mapping_entries, has_line_info);
589591
match selection {
590592
Some(NoLineSelection::Single(member)) => {
@@ -744,7 +746,7 @@ impl<'s> ProguardMapper<'s> {
744746

745747
if let Some(frame) = stacktrace::parse_frame(line) {
746748
if self.is_outline_frame(frame.class, frame.method) {
747-
carried_outline_pos = Some(frame.line);
749+
carried_outline_pos = Some(frame.line.unwrap_or(0));
748750
continue;
749751
}
750752

@@ -810,7 +812,7 @@ impl<'s> ProguardMapper<'s> {
810812
let mut next_frame_can_rewrite = exception_descriptor.is_some();
811813
for f in trace.frames.iter() {
812814
if self.is_outline_frame(f.class, f.method) {
813-
carried_outline_pos = Some(f.line);
815+
carried_outline_pos = Some(f.line.unwrap_or(0));
814816
continue;
815817
}
816818

@@ -913,15 +915,15 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g:
913915
StackFrame {
914916
class: "com.example.MainFragment$g",
915917
method: "onClick",
916-
line: 2,
918+
line: Some(2),
917919
file: Some(Cow::Borrowed("SourceFile")),
918920
parameters: None,
919921
method_synthesized: false,
920922
},
921923
StackFrame {
922924
class: "android.view.View",
923925
method: "performClick",
924-
line: 7393,
926+
line: Some(7393),
925927
file: Some(Cow::Borrowed("View.java")),
926928
parameters: None,
927929
method_synthesized: false,
@@ -935,7 +937,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g:
935937
frames: vec![StackFrame {
936938
class: "com.example.MainFragment$g",
937939
method: "onClick",
938-
line: 1,
940+
line: Some(1),
939941
file: Some(Cow::Borrowed("SourceFile")),
940942
parameters: None,
941943
method_synthesized: false,
@@ -1055,7 +1057,7 @@ some.Class -> a:
10551057
frames: vec![StackFrame {
10561058
class: "a",
10571059
method: "a",
1058-
line: 4,
1060+
line: Some(4),
10591061
file: Some(Cow::Borrowed("SourceFile")),
10601062
parameters: None,
10611063
method_synthesized: false,
@@ -1069,7 +1071,7 @@ some.Class -> a:
10691071
assert_eq!(remapped.frames.len(), 1);
10701072
assert_eq!(remapped.frames[0].class, "some.Class");
10711073
assert_eq!(remapped.frames[0].method, "caller");
1072-
assert_eq!(remapped.frames[0].line, 7);
1074+
assert_eq!(remapped.frames[0].line, Some(7));
10731075
}
10741076

10751077
#[test]
@@ -1176,15 +1178,15 @@ some.Other -> b:
11761178
StackFrame {
11771179
class: "a",
11781180
method: "call",
1179-
line: 4,
1181+
line: Some(4),
11801182
file: Some(Cow::Borrowed("SourceFile")),
11811183
parameters: None,
11821184
method_synthesized: false,
11831185
},
11841186
StackFrame {
11851187
class: "b",
11861188
method: "run",
1187-
line: 5,
1189+
line: Some(5),
11881190
file: Some(Cow::Borrowed("SourceFile")),
11891191
parameters: None,
11901192
method_synthesized: false,
@@ -1200,6 +1202,6 @@ some.Other -> b:
12001202
assert_eq!(remapped.frames.len(), 1);
12011203
assert_eq!(remapped.frames[0].class, "some.Other");
12021204
assert_eq!(remapped.frames[0].method, "method");
1203-
assert_eq!(remapped.frames[0].line, 30);
1205+
assert_eq!(remapped.frames[0].line, Some(30));
12041206
}
12051207
}

0 commit comments

Comments
 (0)