Skip to content

Commit f32d990

Browse files
romtsnclaude
andauthored
fix(mapping): Synthesize filenames for Kotlin classes buried in synthetic wrappers (#94)
Fix synthesized source filenames for Compose-compiler-generated `ComposableSingletons$FileKt` classes when they are inlined through R8 synthetic lambdas. When a Compose compilation emits a `ComposableSingletons$MainActivityKt` class, the mapping often does not include a top-level declaration for it — only its `$$ExternalSyntheticLambda*` siblings appear, each carrying `sourceFile: R8$$SyntheticClass`. In that case the outer Kotlin class has no `sourceFile` metadata of its own, and frame resolution falls back to `synthesize_source_file`, which previously did `split('$').next()` and collapsed `ComposableSingletons$MainActivityKt` to `ComposableSingletons`. The resulting frame's filename became `ComposableSingletons.java`, dropping the real `MainActivity.kt`. The fix scans every `$`-separated segment of the last dotted component for a Kotlin file-class marker (ends in `Kt`). The inner `MainActivityKt` segment now wins and produces `MainActivity.kt`. Bare top-level Kotlin classes (`FooKt`) and non-Kotlin inner classes (`Foo$Bar` → `Foo.java`) are unchanged — they still resolve on the first iteration exactly as before. Includes a minimal reproducer in `tests/r8-source-file-edge-cases.rs` mirroring the scenario: a `ComposableSingletons$MyScreenKt` lambda inlined through an `$$ExternalSyntheticLambda` whose `sourceFile` is `R8$$SyntheticClass`. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 28dfb00 commit f32d990

2 files changed

Lines changed: 105 additions & 12 deletions

File tree

src/utils.rs

Lines changed: 78 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,27 @@ pub(crate) fn extract_class_name(full_path: &str) -> Option<&str> {
77
}
88

99
/// Synthesizes a source file name from a class name.
10-
/// For Kotlin top-level classes ending in "Kt", the suffix is stripped and ".kt" is used.
11-
/// Otherwise, the extension is derived from the reference file, defaulting to ".java".
12-
/// For example: ("com.example.MainKt", Some("Other.java")) -> "Main.kt" (Kt suffix takes precedence)
13-
/// For example: ("com.example.Main", Some("Other.kt")) -> "Main.kt"
14-
/// For example: ("com.example.MainKt", None) -> "Main.kt"
15-
/// For inner classes: ("com.example.Main$Inner", None) -> "Main.java"
10+
///
11+
/// Any `$`-segment ending in `Kt` wins with a `.kt` extension — this covers
12+
/// top-level Kotlin classes and Compose-compiler wrappers alike. Otherwise the
13+
/// extension comes from `reference_file`, defaulting to `.java`.
14+
///
15+
/// See the tests at the bottom of this file for the full matrix of cases.
1616
pub(crate) fn synthesize_source_file(
1717
class_name: &str,
1818
reference_file: Option<&str>,
1919
) -> Option<String> {
20-
let base = extract_class_name(class_name)?;
20+
let last_segment = class_name.split('.').next_back()?;
21+
let mut segments = last_segment.split('$');
22+
let base = segments.next()?;
2123

22-
// For Kotlin top-level classes (ending in "Kt"), always use .kt extension and strip suffix
23-
// This takes precedence over reference_file since Kt suffix is a strong Kotlin indicator
24-
if base.ends_with("Kt") && base.len() > 2 {
25-
let kotlin_base = &base[..base.len() - 2];
26-
return Some(format!("{}.kt", kotlin_base));
24+
// Kt suffix is a strong Kotlin indicator and takes precedence over reference_file.
25+
// Compiler-generated wrappers (e.g. `ComposableSingletons$MainActivityKt`) bury the
26+
// marker in an inner segment, so we scan every `$`-segment, not just `base`.
27+
for segment in std::iter::once(base).chain(segments) {
28+
if let Some(kotlin_base) = segment.strip_suffix("Kt").filter(|s| !s.is_empty()) {
29+
return Some(format!("{}.kt", kotlin_base));
30+
}
2731
}
2832

2933
// If we have a reference file, derive extension from it
@@ -44,3 +48,65 @@ pub fn class_name_to_descriptor(class: &str) -> String {
4448
descriptor.push(';');
4549
descriptor
4650
}
51+
52+
#[cfg(test)]
53+
mod tests {
54+
use super::synthesize_source_file;
55+
56+
#[test]
57+
fn kotlin_top_level_class_uses_kt_extension() {
58+
assert_eq!(
59+
synthesize_source_file("com.example.MainKt", None).as_deref(),
60+
Some("Main.kt")
61+
);
62+
}
63+
64+
#[test]
65+
fn kt_suffix_takes_precedence_over_reference_file() {
66+
assert_eq!(
67+
synthesize_source_file("com.example.MainKt", Some("Other.java")).as_deref(),
68+
Some("Main.kt")
69+
);
70+
}
71+
72+
#[test]
73+
fn reference_file_extension_is_used_when_no_kt_suffix() {
74+
assert_eq!(
75+
synthesize_source_file("com.example.Main", Some("Other.kt")).as_deref(),
76+
Some("Main.kt")
77+
);
78+
}
79+
80+
#[test]
81+
fn non_kotlin_inner_class_falls_back_to_outer_java() {
82+
assert_eq!(
83+
synthesize_source_file("com.example.Main$Inner", None).as_deref(),
84+
Some("Main.java")
85+
);
86+
}
87+
88+
#[test]
89+
fn composable_singletons_wrapper_uses_inner_kt_segment() {
90+
assert_eq!(
91+
synthesize_source_file("com.example.ComposableSingletons$MainKt", None).as_deref(),
92+
Some("Main.kt")
93+
);
94+
}
95+
96+
#[test]
97+
fn bare_kt_segment_is_not_a_kotlin_marker() {
98+
// Degenerate "Kt" alone (length 2) must not strip to an empty base.
99+
assert_eq!(
100+
synthesize_source_file("com.example.Foo$Kt", None).as_deref(),
101+
Some("Foo.java")
102+
);
103+
}
104+
105+
#[test]
106+
fn default_extension_is_java() {
107+
assert_eq!(
108+
synthesize_source_file("com.example.Main", None).as_deref(),
109+
Some("Main.java")
110+
);
111+
}
112+
}

tests/r8-source-file-edge-cases.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,33 @@ fn test_source_file_name_synthesize_stacktrace() {
117117
assert_remap_stacktrace(SOURCE_FILE_NAME_SYNTHESIZE_MAPPING, input, expected);
118118
}
119119

120+
/// ComposableSingletons inlined lambda
121+
///
122+
/// When a lambda from a Compose `ComposableSingletons$FileKt` wrapper is inlined
123+
/// through an R8 synthetic class, the outer class has no `sourceFile` metadata of
124+
/// its own (only its `$$ExternalSyntheticLambda*` children appear in the mapping,
125+
/// carrying `R8$$SyntheticClass`). The synthesized filename must be driven by the
126+
/// inner `FooKt` segment, not by stripping after the first `$`.
127+
const COMPOSABLE_SINGLETONS_INLINED_LAMBDA_MAPPING: &str = "\
128+
host.LocalKt$$ExternalSyntheticLambda0 -> a.b:
129+
# {\"id\":\"sourceFile\",\"fileName\":\"R8$$SyntheticClass\"}
130+
1:1:kotlin.Unit io.example.ComposableSingletons$MyScreenKt.lambda_1$lambda$2():42:42 -> a
131+
";
132+
133+
#[test]
134+
fn test_composable_singletons_inlined_lambda_stacktrace() {
135+
let input = " at a.b.a(SourceFile:1)\n";
136+
137+
let expected =
138+
" at io.example.ComposableSingletons$MyScreenKt.lambda_1$lambda$2(MyScreen.kt:42)\n";
139+
140+
assert_remap_stacktrace(
141+
COMPOSABLE_SINGLETONS_INLINED_LAMBDA_MAPPING,
142+
input,
143+
expected,
144+
);
145+
}
146+
120147
// =============================================================================
121148
// SourceFileWithNumberAndEmptyStackTrace
122149
// =============================================================================

0 commit comments

Comments
 (0)