Skip to content

Commit e0ca873

Browse files
romtsnclaude
andauthored
feat(r8): Propagate class-level synthesized flag to members (#73)
Part of #40 Propagates the class-level `is_synthesized` flag (from `# {"id":"com.android.tools.r8.synthesized"}` headers) to all members of that class. This allows callers (e.g. Sentry symbolicator) to filter out synthetic frames via `method_synthesized()` — this crate intentionally keeps them in the output. ## Changes - **mapper.rs / cache/raw.rs**: OR class-level and method-level synthesized flags when resolving mappings - **tests/r8-synthetic.rs**: 7 tests covering lambda methods, lambda with inlining, and moved synthesized info — including both mapper and cache paths, stacktrace remapping, and `method_synthesized()` flag verification --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ebd61d1 commit e0ca873

File tree

3 files changed

+263
-3
lines changed

3 files changed

+263
-3
lines changed

src/cache/raw.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,11 @@ impl<'data> ProguardCache<'data> {
577577
.get(&member.method)
578578
.copied()
579579
.unwrap_or_default();
580-
let is_synthesized = method_info.is_synthesized as u8;
580+
let class_synthesized = parsed
581+
.class_infos
582+
.get(&member.method.receiver.name())
583+
.is_some_and(|ci| ci.is_synthesized);
584+
let is_synthesized = (method_info.is_synthesized || class_synthesized) as u8;
581585
let is_outline = method_info.is_outline as u8;
582586

583587
let outline_pairs: Vec<OutlinePair> = member

src/mapper.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ struct ClassMapping<'s> {
9595
members: HashMap<&'s str, ClassMembers<'s>>,
9696
#[expect(
9797
unused,
98-
reason = "It is currently unknown what effect a synthesized class has."
98+
reason = "Class-level synthesized is propagated to members in resolve_mapping; \
99+
kept here for potential future class-level queries."
99100
)]
100101
is_synthesized: bool,
101102
}
@@ -549,7 +550,13 @@ impl<'s> ProguardMapper<'s> {
549550
.get(&member.method)
550551
.copied()
551552
.unwrap_or_default();
552-
let is_synthesized = method_info.is_synthesized;
553+
// A member is considered synthesized if either its own method info
554+
// or its owning class is marked synthesized.
555+
let class_synthesized = parsed
556+
.class_infos
557+
.get(&member.method.receiver.name())
558+
.is_some_and(|ci| ci.is_synthesized);
559+
let is_synthesized = method_info.is_synthesized || class_synthesized;
553560
let is_outline = method_info.is_outline;
554561

555562
let outline_callsite_positions = member.outline_callsite_positions.clone();

tests/r8-synthetic.rs

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
//! Tests for R8 synthetic / lambda method retracing fixtures.
2+
//!
3+
//! These tests are based on the R8 retrace test suite from:
4+
//! src/test/java/com/android/tools/r8/retrace/stacktraces/
5+
//!
6+
//! Note: this crate does NOT filter out synthesized frames. Instead it
7+
//! propagates the `method_synthesized` flag so that callers (e.g. Sentry
8+
//! symbolicator) can decide whether to strip them.
9+
10+
use proguard::{ProguardCache, ProguardMapper, ProguardMapping, StackFrame};
11+
12+
// =============================================================================
13+
// SyntheticLambdaMethodStackTrace
14+
// =============================================================================
15+
16+
const SYNTHETIC_LAMBDA_METHOD_MAPPING: &str = "\
17+
# {\"id\":\"com.android.tools.r8.mapping\",\"version\":\"1.0\"}
18+
example.Main -> example.Main:
19+
1:1:void main(java.lang.String[]):123 -> main
20+
example.Foo -> a.a:
21+
5:5:void lambda$main$0():225 -> a
22+
3:3:void runIt():218 -> b
23+
2:2:void main():223 -> c
24+
example.Foo$$ExternalSyntheticLambda0 -> a.b:
25+
void run(example.Foo) -> a
26+
# {\"id\":\"com.android.tools.r8.synthesized\"}
27+
";
28+
29+
#[test]
30+
fn test_synthetic_lambda_method_stacktrace() {
31+
let input = "\
32+
Exception in thread \"main\" java.lang.NullPointerException
33+
at a.a.a(a.java:5)
34+
at a.b.a(Unknown Source)
35+
at a.a.b(a.java:3)
36+
at a.a.c(a.java:2)
37+
at example.Main.main(Main.java:1)
38+
";
39+
40+
// Synthetic frames are kept in the output; callers filter via method_synthesized().
41+
let expected = "\
42+
Exception in thread \"main\" java.lang.NullPointerException
43+
at example.Foo.lambda$main$0(Foo.java:225)
44+
at example.Foo$$ExternalSyntheticLambda0.run(Foo.java:0)
45+
at example.Foo.runIt(Foo.java:218)
46+
at example.Foo.main(Foo.java:223)
47+
at example.Main.main(Main.java:123)
48+
";
49+
50+
let mapper = ProguardMapper::from(SYNTHETIC_LAMBDA_METHOD_MAPPING);
51+
let actual = mapper.remap_stacktrace(input).unwrap();
52+
assert_eq!(actual.trim(), expected.trim());
53+
54+
let mapping = ProguardMapping::new(SYNTHETIC_LAMBDA_METHOD_MAPPING.as_bytes());
55+
let mut buf = Vec::new();
56+
ProguardCache::write(&mapping, &mut buf).unwrap();
57+
let cache = ProguardCache::parse(&buf).unwrap();
58+
cache.test();
59+
60+
let actual = cache.remap_stacktrace(input).unwrap();
61+
assert_eq!(actual.trim(), expected.trim());
62+
}
63+
64+
#[test]
65+
fn test_synthetic_lambda_method_synthesized_flag() {
66+
let mapper = ProguardMapper::from(SYNTHETIC_LAMBDA_METHOD_MAPPING);
67+
68+
// The synthetic lambda class member should have method_synthesized = true.
69+
let frame = StackFrame::try_parse(" at a.b.a(Unknown Source)".as_bytes()).unwrap();
70+
let remapped: Vec<_> = mapper.remap_frame(&frame).collect();
71+
assert!(
72+
remapped.iter().all(|f| f.method_synthesized()),
73+
"expected all frames from synthetic class to have method_synthesized = true, got: {remapped:?}"
74+
);
75+
76+
// A regular method should have method_synthesized = false.
77+
let frame = StackFrame::try_parse(" at a.a.a(a.java:5)".as_bytes()).unwrap();
78+
let remapped: Vec<_> = mapper.remap_frame(&frame).collect();
79+
assert!(
80+
remapped.iter().all(|f| !f.method_synthesized()),
81+
"expected regular frame to have method_synthesized = false, got: {remapped:?}"
82+
);
83+
}
84+
85+
#[test]
86+
fn test_synthetic_lambda_method_synthesized_flag_cache() {
87+
let mapping = ProguardMapping::new(SYNTHETIC_LAMBDA_METHOD_MAPPING.as_bytes());
88+
let mut buf = Vec::new();
89+
ProguardCache::write(&mapping, &mut buf).unwrap();
90+
let cache = ProguardCache::parse(&buf).unwrap();
91+
92+
// The synthetic lambda class member should have method_synthesized = true.
93+
let frame = StackFrame::try_parse(" at a.b.a(Unknown Source)".as_bytes()).unwrap();
94+
let remapped: Vec<_> = cache.remap_frame(&frame).collect();
95+
assert!(
96+
remapped.iter().all(|f| f.method_synthesized()),
97+
"cache: expected synthetic frame to have method_synthesized = true, got: {remapped:?}"
98+
);
99+
100+
// A regular method should have method_synthesized = false.
101+
let frame = StackFrame::try_parse(" at a.a.a(a.java:5)".as_bytes()).unwrap();
102+
let remapped: Vec<_> = cache.remap_frame(&frame).collect();
103+
assert!(
104+
remapped.iter().all(|f| !f.method_synthesized()),
105+
"cache: expected regular frame to have method_synthesized = false, got: {remapped:?}"
106+
);
107+
}
108+
109+
// =============================================================================
110+
// SyntheticLambdaMethodWithInliningStackTrace
111+
// =============================================================================
112+
113+
const SYNTHETIC_LAMBDA_METHOD_WITH_INLINING_MAPPING: &str = "\
114+
# {\"id\":\"com.android.tools.r8.mapping\",\"version\":\"1.0\"}
115+
example.Main -> example.Main:
116+
1:1:void main(java.lang.String[]):123 -> main
117+
example.Foo -> a.a:
118+
3:3:void runIt():218 -> b
119+
2:2:void main():223 -> c
120+
example.Foo$$ExternalSyntheticLambda0 -> a.b:
121+
4:4:void example.Foo.lambda$main$0():225 -> a
122+
4:4:void run(example.Foo):0 -> a
123+
# {\"id\":\"com.android.tools.r8.synthesized\"}
124+
";
125+
126+
#[test]
127+
fn test_synthetic_lambda_method_with_inlining_stacktrace() {
128+
let input = "\
129+
Exception in thread \"main\" java.lang.NullPointerException
130+
at a.b.a(Unknown Source:4)
131+
at a.a.b(a.java:3)
132+
at a.a.c(a.java:2)
133+
at example.Main.main(Main.java:1)
134+
";
135+
136+
// Synthetic frames are kept; the inlined lambda$main$0 is not synthetic,
137+
// but the outer run() method is.
138+
let expected = "\
139+
Exception in thread \"main\" java.lang.NullPointerException
140+
at example.Foo.lambda$main$0(Foo.java:225)
141+
at example.Foo$$ExternalSyntheticLambda0.run(Foo.java:0)
142+
at example.Foo.runIt(Foo.java:218)
143+
at example.Foo.main(Foo.java:223)
144+
at example.Main.main(Main.java:123)
145+
";
146+
147+
let mapper = ProguardMapper::from(SYNTHETIC_LAMBDA_METHOD_WITH_INLINING_MAPPING);
148+
let actual = mapper.remap_stacktrace(input).unwrap();
149+
assert_eq!(actual.trim(), expected.trim());
150+
151+
let mapping = ProguardMapping::new(SYNTHETIC_LAMBDA_METHOD_WITH_INLINING_MAPPING.as_bytes());
152+
let mut buf = Vec::new();
153+
ProguardCache::write(&mapping, &mut buf).unwrap();
154+
let cache = ProguardCache::parse(&buf).unwrap();
155+
cache.test();
156+
157+
let actual = cache.remap_stacktrace(input).unwrap();
158+
assert_eq!(actual.trim(), expected.trim());
159+
}
160+
161+
#[test]
162+
fn test_synthetic_lambda_method_with_inlining_synthesized_flag() {
163+
let mapper = ProguardMapper::from(SYNTHETIC_LAMBDA_METHOD_WITH_INLINING_MAPPING);
164+
165+
// Inline expansion from a synthetic class: the run() member is synthesized
166+
// but the inlined lambda$main$0 is from example.Foo (not synthesized).
167+
let frame = StackFrame::try_parse(" at a.b.a(Unknown Source:4)".as_bytes()).unwrap();
168+
let remapped: Vec<_> = mapper.remap_frame(&frame).collect();
169+
assert_eq!(remapped.len(), 2);
170+
// lambda$main$0 is from example.Foo — not synthesized
171+
assert!(
172+
!remapped[0].method_synthesized(),
173+
"inlined frame should not be synthesized"
174+
);
175+
// run() is from the synthetic class — synthesized
176+
assert!(
177+
remapped[1].method_synthesized(),
178+
"outer synthetic frame should be synthesized"
179+
);
180+
}
181+
182+
// =============================================================================
183+
// MovedSynthetizedInfoStackTraceTest
184+
// =============================================================================
185+
186+
const MOVED_SYNTHETIZED_INFO_MAPPING: &str = "\
187+
# {\"id\":\"com.android.tools.r8.mapping\",\"version\":\"2.2\"}
188+
com.android.tools.r8.BaseCommand$Builder -> foo.bar:
189+
1:1:void inlinee(java.util.Collection):0:0 -> inlinee$synthetic
190+
1:1:void inlinee$synthetic(java.util.Collection):0:0 -> inlinee$synthetic
191+
2:2:void inlinee(java.util.Collection):206:206 -> inlinee$synthetic
192+
2:2:void inlinee$synthetic(java.util.Collection):0:0 -> inlinee$synthetic
193+
# {\"id\":\"com.android.tools.r8.synthesized\"}
194+
4:4:void inlinee(java.util.Collection):208:208 -> inlinee$synthetic
195+
4:4:void inlinee$synthetic(java.util.Collection):0 -> inlinee$synthetic
196+
7:7:void error(origin.Origin,java.lang.Throwable):363:363 -> inlinee$synthetic
197+
7:7:void inlinee(java.util.Collection):210 -> inlinee$synthetic
198+
7:7:void inlinee$synthetic(java.util.Collection):0:0 -> inlinee$synthetic
199+
";
200+
201+
#[test]
202+
fn test_moved_synthetized_info_stacktrace() {
203+
let input = "\
204+
java.lang.RuntimeException: foobar
205+
\tat foo.bar.inlinee$synthetic(BaseCommand.java:2)
206+
";
207+
208+
// The inlined pair at line 2: inlinee (original:206) + inlinee$synthetic (original:0).
209+
// The inlinee$synthetic method is marked synthesized; inlinee is not.
210+
let expected = "\
211+
java.lang.RuntimeException: foobar
212+
at com.android.tools.r8.BaseCommand$Builder.inlinee(BaseCommand.java:206)
213+
at com.android.tools.r8.BaseCommand$Builder.inlinee$synthetic(BaseCommand.java:0)
214+
";
215+
216+
let mapper = ProguardMapper::from(MOVED_SYNTHETIZED_INFO_MAPPING);
217+
let actual = mapper.remap_stacktrace(input).unwrap();
218+
assert_eq!(actual.trim(), expected.trim());
219+
220+
let mapping = ProguardMapping::new(MOVED_SYNTHETIZED_INFO_MAPPING.as_bytes());
221+
let mut buf = Vec::new();
222+
ProguardCache::write(&mapping, &mut buf).unwrap();
223+
let cache = ProguardCache::parse(&buf).unwrap();
224+
cache.test();
225+
226+
let actual = cache.remap_stacktrace(input).unwrap();
227+
assert_eq!(actual.trim(), expected.trim());
228+
}
229+
230+
#[test]
231+
fn test_moved_synthetized_info_synthesized_flag() {
232+
let mapper = ProguardMapper::from(MOVED_SYNTHETIZED_INFO_MAPPING);
233+
234+
let frame =
235+
StackFrame::try_parse("\tat foo.bar.inlinee$synthetic(BaseCommand.java:2)".as_bytes())
236+
.unwrap();
237+
let remapped: Vec<_> = mapper.remap_frame(&frame).collect();
238+
assert_eq!(remapped.len(), 2);
239+
// inlinee — not synthesized
240+
assert!(
241+
!remapped[0].method_synthesized(),
242+
"inlinee should not be synthesized"
243+
);
244+
// inlinee$synthetic — synthesized
245+
assert!(
246+
remapped[1].method_synthesized(),
247+
"inlinee$synthetic should be synthesized"
248+
);
249+
}

0 commit comments

Comments
 (0)