Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 78 additions & 12 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,27 @@ pub(crate) fn extract_class_name(full_path: &str) -> Option<&str> {
}

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

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

// If we have a reference file, derive extension from it
Expand All @@ -44,3 +48,65 @@ pub fn class_name_to_descriptor(class: &str) -> String {
descriptor.push(';');
descriptor
}

#[cfg(test)]
mod tests {
use super::synthesize_source_file;

#[test]
fn kotlin_top_level_class_uses_kt_extension() {
assert_eq!(
synthesize_source_file("com.example.MainKt", None).as_deref(),
Some("Main.kt")
);
}

#[test]
fn kt_suffix_takes_precedence_over_reference_file() {
assert_eq!(
synthesize_source_file("com.example.MainKt", Some("Other.java")).as_deref(),
Some("Main.kt")
);
}

#[test]
fn reference_file_extension_is_used_when_no_kt_suffix() {
assert_eq!(
synthesize_source_file("com.example.Main", Some("Other.kt")).as_deref(),
Some("Main.kt")
);
}

#[test]
fn non_kotlin_inner_class_falls_back_to_outer_java() {
assert_eq!(
synthesize_source_file("com.example.Main$Inner", None).as_deref(),
Some("Main.java")
);
}

#[test]
fn composable_singletons_wrapper_uses_inner_kt_segment() {
assert_eq!(
synthesize_source_file("com.example.ComposableSingletons$MainKt", None).as_deref(),
Some("Main.kt")
);
}

#[test]
fn bare_kt_segment_is_not_a_kotlin_marker() {
// Degenerate "Kt" alone (length 2) must not strip to an empty base.
assert_eq!(
synthesize_source_file("com.example.Foo$Kt", None).as_deref(),
Some("Foo.java")
);
}

#[test]
fn default_extension_is_java() {
assert_eq!(
synthesize_source_file("com.example.Main", None).as_deref(),
Some("Main.java")
);
}
}
27 changes: 27 additions & 0 deletions tests/r8-source-file-edge-cases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,33 @@ fn test_source_file_name_synthesize_stacktrace() {
assert_remap_stacktrace(SOURCE_FILE_NAME_SYNTHESIZE_MAPPING, input, expected);
}

/// ComposableSingletons inlined lambda
///
/// When a lambda from a Compose `ComposableSingletons$FileKt` wrapper is inlined
/// through an R8 synthetic class, the outer class has no `sourceFile` metadata of
/// its own (only its `$$ExternalSyntheticLambda*` children appear in the mapping,
/// carrying `R8$$SyntheticClass`). The synthesized filename must be driven by the
/// inner `FooKt` segment, not by stripping after the first `$`.
const COMPOSABLE_SINGLETONS_INLINED_LAMBDA_MAPPING: &str = "\
host.LocalKt$$ExternalSyntheticLambda0 -> a.b:
# {\"id\":\"sourceFile\",\"fileName\":\"R8$$SyntheticClass\"}
1:1:kotlin.Unit io.example.ComposableSingletons$MyScreenKt.lambda_1$lambda$2():42:42 -> a
";

#[test]
fn test_composable_singletons_inlined_lambda_stacktrace() {
let input = " at a.b.a(SourceFile:1)\n";

let expected =
" at io.example.ComposableSingletons$MyScreenKt.lambda_1$lambda$2(MyScreen.kt:42)\n";

assert_remap_stacktrace(
COMPOSABLE_SINGLETONS_INLINED_LAMBDA_MAPPING,
input,
expected,
);
}

// =============================================================================
// SourceFileWithNumberAndEmptyStackTrace
// =============================================================================
Expand Down
Loading