Skip to content

Commit f132a33

Browse files
romtsnclaude
andauthored
refactor(stacktrace): Change StackFrame.line from usize to Option<usize> (#83)
## Summary - Changes `StackFrame.line` from `usize` to `Option<usize>` to distinguish "no line info" (`None`) from "line 0" (`Some(0)`) - This is a prerequisite for correctly handling R8's various no-line mapping semantics - All call sites updated with `unwrap_or(0)` to preserve current behavior; no semantic changes ## Test plan - [x] All existing tests pass (no regressions) - [x] `test_multiple_lines_no_line_number_stacktrace` now passes as a bonus (was previously failing) - [x] 5 known-failing r8-line-number-handling tests remain (will be fixed in subsequent PRs) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7916939 commit f132a33

File tree

5 files changed

+100
-92
lines changed

5 files changed

+100
-92
lines changed

src/cache/mod.rs

Lines changed: 24 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,9 @@ 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 && pf_line <= member.endline as usize)
395395
{
396396
had_mappings = true;
397397
rewrite_rules.extend(self.decode_rewrite_rules(member));
@@ -477,7 +477,7 @@ impl<'data> ProguardCache<'data> {
477477
'r: 'data,
478478
{
479479
if self.is_outline_frame(frame.class, frame.method) {
480-
*carried_outline_pos = Some(frame.line);
480+
*carried_outline_pos = Some(frame.line.unwrap_or(0));
481481
return None;
482482
}
483483

@@ -678,11 +678,11 @@ impl<'data> ProguardCache<'data> {
678678
if let Some(mapped) = self.map_outline_position(
679679
effective.class,
680680
effective.method,
681-
effective.line,
681+
effective.line.unwrap_or(0),
682682
pos,
683683
effective.parameters,
684684
) {
685-
effective.line = mapped;
685+
effective.line = Some(mapped);
686686
}
687687
}
688688

@@ -903,7 +903,7 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> {
903903
let out = if frame.parameters.is_none() {
904904
// If we have no line number, treat it as unknown. If there are base (no-line) mappings
905905
// present, prefer those over line-mapped entries.
906-
if frame.line == 0 {
906+
if frame.line.unwrap_or(0) == 0 {
907907
let selection = select_no_line_members(members.as_slice())?;
908908
let mapped = match selection {
909909
NoLineSelection::Single(member) => {
@@ -983,9 +983,10 @@ fn iterate_with_lines<'a>(
983983
outer_source_file: Option<&str>,
984984
has_line_info: bool,
985985
) -> Option<StackFrame<'a>> {
986+
let frame_line = frame.line.unwrap_or(0);
986987
for member in members {
987988
// 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 {
989+
if has_line_info && frame_line > 0 && member.endline == 0 {
989990
continue;
990991
}
991992
// If the mapping entry has no line range, preserve the input line number (if any).
@@ -994,7 +995,7 @@ fn iterate_with_lines<'a>(
994995
}
995996
// skip any members which do not match our frames line
996997
if member.endline > 0
997-
&& (frame.line < member.startline as usize || frame.line > member.endline as usize)
998+
&& (frame_line < member.startline as usize || frame_line > member.endline as usize)
998999
{
9991000
continue;
10001001
}
@@ -1005,7 +1006,7 @@ fn iterate_with_lines<'a>(
10051006
{
10061007
member.original_startline as usize
10071008
} else {
1008-
member.original_startline as usize + frame.line - member.startline as usize
1009+
member.original_startline as usize + frame_line - member.startline as usize
10091010
};
10101011

10111012
let class = cache
@@ -1035,7 +1036,7 @@ fn iterate_with_lines<'a>(
10351036
class,
10361037
method,
10371038
file,
1038-
line,
1039+
line: Some(line),
10391040
parameters: frame.parameters,
10401041
method_synthesized: member.is_synthesized(),
10411042
});
@@ -1105,12 +1106,12 @@ fn map_member_without_lines<'a>(
11051106
class,
11061107
method,
11071108
file,
1108-
line: resolve_no_line_output_line(
1109-
frame.line,
1109+
line: Some(resolve_no_line_output_line(
1110+
frame.line.unwrap_or(0),
11101111
original_startline,
11111112
member.startline as usize,
11121113
member.endline as usize,
1113-
),
1114+
)),
11141115
parameters: frame.parameters,
11151116
method_synthesized: member.is_synthesized(),
11161117
})
@@ -1142,12 +1143,12 @@ fn iterate_without_lines<'a>(
11421143
class,
11431144
method,
11441145
file,
1145-
line: resolve_no_line_output_line(
1146-
frame.line,
1146+
line: Some(resolve_no_line_output_line(
1147+
frame.line.unwrap_or(0),
11471148
original_startline,
11481149
member.startline as usize,
11491150
member.endline as usize,
1150-
),
1151+
)),
11511152
parameters: frame.parameters,
11521153
method_synthesized: member.is_synthesized(),
11531154
})
@@ -1207,15 +1208,15 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g:
12071208
StackFrame {
12081209
class: "com.example.MainFragment$g",
12091210
method: "onClick",
1210-
line: 2,
1211+
line: Some(2),
12111212
file: Some(Cow::Borrowed("SourceFile")),
12121213
parameters: None,
12131214
method_synthesized: false,
12141215
},
12151216
StackFrame {
12161217
class: "android.view.View",
12171218
method: "performClick",
1218-
line: 7393,
1219+
line: Some(7393),
12191220
file: Some(Cow::Borrowed("View.java")),
12201221
parameters: None,
12211222
method_synthesized: false,
@@ -1229,7 +1230,7 @@ com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g:
12291230
frames: vec![StackFrame {
12301231
class: "com.example.MainFragment$g",
12311232
method: "onClick",
1232-
line: 1,
1233+
line: Some(1),
12331234
file: Some(Cow::Borrowed("SourceFile")),
12341235
parameters: None,
12351236
method_synthesized: false,
@@ -1421,15 +1422,15 @@ some.Other -> b:
14211422
StackFrame {
14221423
class: "a",
14231424
method: "call",
1424-
line: 4,
1425+
line: Some(4),
14251426
file: Some(Cow::Borrowed("SourceFile")),
14261427
parameters: None,
14271428
method_synthesized: false,
14281429
},
14291430
StackFrame {
14301431
class: "b",
14311432
method: "run",
1432-
line: 5,
1433+
line: Some(5),
14331434
file: Some(Cow::Borrowed("SourceFile")),
14341435
parameters: None,
14351436
method_synthesized: false,
@@ -1445,6 +1446,6 @@ some.Other -> b:
14451446
assert_eq!(remapped.frames.len(), 1);
14461447
assert_eq!(remapped.frames[0].class, "some.Other");
14471448
assert_eq!(remapped.frames[0].method, "method");
1448-
assert_eq!(remapped.frames[0].line, 30);
1449+
assert_eq!(remapped.frames[0].line, Some(30));
14491450
}
14501451
}

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)