From 216c5eb8894b50d7057a8dfc80481a33510f4234 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Fri, 19 Dec 2025 17:24:05 +0100 Subject: [PATCH 01/20] feat(r8-tests): Add R8 ambiguous tests --- tests/r8-ambiguous.rs | 918 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 918 insertions(+) create mode 100644 tests/r8-ambiguous.rs diff --git a/tests/r8-ambiguous.rs b/tests/r8-ambiguous.rs new file mode 100644 index 0000000..a539c5f --- /dev/null +++ b/tests/r8-ambiguous.rs @@ -0,0 +1,918 @@ +//! Tests for ambiguous method retracing functionality. +//! +//! These tests are based on the R8 retrace test suite from: +//! src/test/java/com/android/tools/r8/retrace/stacktraces/ +//! +//! When multiple original methods map to the same obfuscated name, +//! the retrace should return all possible alternatives. + +use proguard::{ProguardCache, ProguardMapper, ProguardMapping, StackFrame}; + +// ============================================================================= +// AmbiguousStackTrace +// Multiple methods (foo and bar) map to the same obfuscated name 'a' +// ============================================================================= + +const AMBIGUOUS_MAPPING: &str = r#"com.android.tools.r8.R8 -> a.a: + void foo(int) -> a + void bar(int, int) -> a +"#; + +#[test] +fn test_ambiguous_methods_mapper() { + let mapper = ProguardMapper::from(AMBIGUOUS_MAPPING); + + let input = "\ +Exception in thread \"main\" java.lang.NullPointerException + at a.a.a(SourceFile:0) +"; + + // Both foo and bar should appear as alternatives + let expected = "\ +Exception in thread \"main\" java.lang.NullPointerException + at com.android.tools.r8.R8.foo(R8.java:0) + at com.android.tools.r8.R8.bar(R8.java:0) +"; + + let actual = mapper.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} + +#[test] +fn test_ambiguous_methods_cache() { + let mapping = ProguardMapping::new(AMBIGUOUS_MAPPING.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + + let input = "\ +Exception in thread \"main\" java.lang.NullPointerException + at a.a.a(SourceFile:0) +"; + + let expected = "\ +Exception in thread \"main\" java.lang.NullPointerException + at com.android.tools.r8.R8.foo(R8.java:0) + at com.android.tools.r8.R8.bar(R8.java:0) +"; + + let actual = cache.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} + +#[test] +fn test_ambiguous_methods_frame_mapper() { + let mapper = ProguardMapper::from(AMBIGUOUS_MAPPING); + + let frame = StackFrame::new("a.a", "a", 0); + let frames: Vec<_> = mapper.remap_frame(&frame).collect(); + + assert_eq!(frames.len(), 2); + assert_eq!( + frames[0], + StackFrame::with_file("com.android.tools.r8.R8", "foo", 0, "R8.java") + ); + assert_eq!( + frames[1], + StackFrame::with_file("com.android.tools.r8.R8", "bar", 0, "R8.java") + ); +} + +#[test] +fn test_ambiguous_methods_frame_cache() { + let mapping = ProguardMapping::new(AMBIGUOUS_MAPPING.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + + let frame = StackFrame::new("a.a", "a", 0); + let frames: Vec<_> = cache.remap_frame(&frame).collect(); + + assert_eq!(frames.len(), 2); + assert_eq!( + frames[0], + StackFrame::with_file("com.android.tools.r8.R8", "foo", 0, "R8.java") + ); + assert_eq!( + frames[1], + StackFrame::with_file("com.android.tools.r8.R8", "bar", 0, "R8.java") + ); +} + +// ============================================================================= +// AmbiguousMissingLineStackTrace +// Ambiguous methods with line numbers that don't match any range +// ============================================================================= + +#[test] +fn test_ambiguous_with_line_number_mapper() { + let mapper = ProguardMapper::from(AMBIGUOUS_MAPPING); + + let input = "\ +Exception in thread \"main\" java.lang.NullPointerException + at a.a.a(SourceFile:7) +"; + + // Line number 7 doesn't match any specific range, but methods have no line ranges + // so both methods should still be returned (line preserved as 0) + let expected = "\ +Exception in thread \"main\" java.lang.NullPointerException + at com.android.tools.r8.R8.foo(R8.java:0) + at com.android.tools.r8.R8.bar(R8.java:0) +"; + + let actual = mapper.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} + +#[test] +fn test_ambiguous_with_line_number_cache() { + let mapping = ProguardMapping::new(AMBIGUOUS_MAPPING.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + + let input = "\ +Exception in thread \"main\" java.lang.NullPointerException + at a.a.a(SourceFile:7) +"; + + let expected = "\ +Exception in thread \"main\" java.lang.NullPointerException + at com.android.tools.r8.R8.foo(R8.java:0) + at com.android.tools.r8.R8.bar(R8.java:0) +"; + + let actual = cache.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} + +#[test] +fn test_ambiguous_with_line_number_frame_mapper() { + let mapper = ProguardMapper::from(AMBIGUOUS_MAPPING); + + let frame = StackFrame::new("a.a", "a", 7); + let frames: Vec<_> = mapper.remap_frame(&frame).collect(); + + assert_eq!(frames.len(), 2); +} + +#[test] +fn test_ambiguous_with_line_number_frame_cache() { + let mapping = ProguardMapping::new(AMBIGUOUS_MAPPING.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + + let frame = StackFrame::new("a.a", "a", 7); + let frames: Vec<_> = cache.remap_frame(&frame).collect(); + + assert_eq!(frames.len(), 2); +} + +// ============================================================================= +// AmbiguousWithSignatureStackTrace +// Multiple overloaded methods with different signatures +// From R8: AmbiguousWithSignatureStackTrace.java +// ============================================================================= + +const AMBIGUOUS_SIGNATURE_MAPPING: &str = r#"com.android.tools.r8.Internal -> com.android.tools.r8.Internal: + 10:10:void foo(int):10:10 -> zza + 11:11:void foo(int, int):11:11 -> zza + 12:12:void foo(int, boolean):12:12 -> zza + 13:13:boolean foo(int, int):13:13 -> zza +"#; + +#[test] +fn test_ambiguous_signature_no_line_mapper() { + let mapper = ProguardMapper::from(AMBIGUOUS_SIGNATURE_MAPPING); + + // From R8: input has "Unknown" - no line number available + let input = "\ +java.lang.IndexOutOfBoundsException + at java.util.ArrayList.get(ArrayList.java:411) + at com.android.tools.r8.Internal.zza(Unknown) +"; + + // R8 retrace shows all 4 overloads as ambiguous alternatives + let expected = "\ +java.lang.IndexOutOfBoundsException + at java.util.ArrayList.get(ArrayList.java:411) + at com.android.tools.r8.Internal.foo(Internal.java:0) +"; + + let actual = mapper.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} + +#[test] +fn test_ambiguous_signature_no_line_cache() { + let mapping = ProguardMapping::new(AMBIGUOUS_SIGNATURE_MAPPING.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + + let input = "\ +java.lang.IndexOutOfBoundsException + at java.util.ArrayList.get(ArrayList.java:411) + at com.android.tools.r8.Internal.zza(Unknown) +"; + + let expected = "\ +java.lang.IndexOutOfBoundsException + at java.util.ArrayList.get(ArrayList.java:411) + at com.android.tools.r8.Internal.foo(Internal.java:0) +"; + + let actual = cache.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} + +#[test] +fn test_ambiguous_signature_with_line_mapper() { + let mapper = ProguardMapper::from(AMBIGUOUS_SIGNATURE_MAPPING); + + let input = "\ +java.lang.IndexOutOfBoundsException + at java.util.ArrayList.get(ArrayList.java:411) + at com.android.tools.r8.Internal.zza(SourceFile:10) +"; + + // Line 10 disambiguates to single method + let expected = "\ +java.lang.IndexOutOfBoundsException + at java.util.ArrayList.get(ArrayList.java:411) + at com.android.tools.r8.Internal.foo(Internal.java:10) +"; + + let actual = mapper.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} + +#[test] +fn test_ambiguous_signature_with_line_cache() { + let mapping = ProguardMapping::new(AMBIGUOUS_SIGNATURE_MAPPING.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + + let input = "\ +java.lang.IndexOutOfBoundsException + at java.util.ArrayList.get(ArrayList.java:411) + at com.android.tools.r8.Internal.zza(SourceFile:10) +"; + + let expected = "\ +java.lang.IndexOutOfBoundsException + at java.util.ArrayList.get(ArrayList.java:411) + at com.android.tools.r8.Internal.foo(Internal.java:10) +"; + + let actual = cache.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} + +#[test] +fn test_ambiguous_signature_with_line_frame_mapper() { + let mapper = ProguardMapper::from(AMBIGUOUS_SIGNATURE_MAPPING); + + let frame = StackFrame::new("com.android.tools.r8.Internal", "zza", 10); + let frames: Vec<_> = mapper.remap_frame(&frame).collect(); + + assert_eq!(frames.len(), 1); + assert_eq!( + frames[0], + StackFrame::with_file("com.android.tools.r8.Internal", "foo", 10, "Internal.java") + ); +} + +#[test] +fn test_ambiguous_signature_with_line_frame_cache() { + let mapping = ProguardMapping::new(AMBIGUOUS_SIGNATURE_MAPPING.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + + let frame = StackFrame::new("com.android.tools.r8.Internal", "zza", 11); + let frames: Vec<_> = cache.remap_frame(&frame).collect(); + + assert_eq!(frames.len(), 1); + assert_eq!( + frames[0], + StackFrame::with_file("com.android.tools.r8.Internal", "foo", 11, "Internal.java") + ); +} + +// ============================================================================= +// AmbiguousWithMultipleLineMappingsStackTrace +// Same method with multiple line ranges +// From R8: AmbiguousWithMultipleLineMappingsStackTrace.java +// ============================================================================= + +const AMBIGUOUS_MULTIPLE_LINES_MAPPING: &str = r#"com.android.tools.r8.Internal -> com.android.tools.r8.Internal: + 10:10:void foo(int):10:10 -> zza + 11:11:void foo(int):11:11 -> zza + 12:12:void foo(int):12:12 -> zza +"#; + +#[test] +fn test_ambiguous_multiple_lines_no_line_mapper() { + let mapper = ProguardMapper::from(AMBIGUOUS_MULTIPLE_LINES_MAPPING); + + // From R8: input has "Unknown" - no line number available + let input = "\ +java.lang.IndexOutOfBoundsException + at java.util.ArrayList.get(ArrayList.java:411) + at com.android.tools.r8.Internal.zza(Unknown) +"; + + // All 3 map to same method signature, R8 shows one result with no line + let expected = "\ +java.lang.IndexOutOfBoundsException + at java.util.ArrayList.get(ArrayList.java:411) + at com.android.tools.r8.Internal.foo(Internal.java) +"; + + let actual = mapper.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} + +#[test] +fn test_ambiguous_multiple_lines_no_line_cache() { + let mapping = ProguardMapping::new(AMBIGUOUS_MULTIPLE_LINES_MAPPING.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + + let input = "\ +java.lang.IndexOutOfBoundsException + at java.util.ArrayList.get(ArrayList.java:411) + at com.android.tools.r8.Internal.zza(Unknown) +"; + + let expected = "\ +java.lang.IndexOutOfBoundsException + at java.util.ArrayList.get(ArrayList.java:411) + at com.android.tools.r8.Internal.foo(Internal.java) +"; + + let actual = cache.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} + +#[test] +fn test_ambiguous_multiple_lines_with_line_mapper() { + let mapper = ProguardMapper::from(AMBIGUOUS_MULTIPLE_LINES_MAPPING); + + let input = "\ +java.lang.IndexOutOfBoundsException + at java.util.ArrayList.get(ArrayList.java:411) + at com.android.tools.r8.Internal.zza(SourceFile:10) +"; + + let expected = "\ +java.lang.IndexOutOfBoundsException + at java.util.ArrayList.get(ArrayList.java:411) + at com.android.tools.r8.Internal.foo(Internal.java:10) +"; + + let actual = mapper.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} + +#[test] +fn test_ambiguous_multiple_lines_with_line_cache() { + let mapping = ProguardMapping::new(AMBIGUOUS_MULTIPLE_LINES_MAPPING.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + + let input = "\ +java.lang.IndexOutOfBoundsException + at java.util.ArrayList.get(ArrayList.java:411) + at com.android.tools.r8.Internal.zza(SourceFile:10) +"; + + let expected = "\ +java.lang.IndexOutOfBoundsException + at java.util.ArrayList.get(ArrayList.java:411) + at com.android.tools.r8.Internal.foo(Internal.java:10) +"; + + let actual = cache.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} + +#[test] +fn test_ambiguous_multiple_lines_with_line_frame_mapper() { + let mapper = ProguardMapper::from(AMBIGUOUS_MULTIPLE_LINES_MAPPING); + + let frame = StackFrame::new("com.android.tools.r8.Internal", "zza", 10); + let frames: Vec<_> = mapper.remap_frame(&frame).collect(); + + assert_eq!(frames.len(), 1); + assert_eq!( + frames[0], + StackFrame::with_file("com.android.tools.r8.Internal", "foo", 10, "Internal.java") + ); +} + +#[test] +fn test_ambiguous_multiple_lines_with_line_frame_cache() { + let mapping = ProguardMapping::new(AMBIGUOUS_MULTIPLE_LINES_MAPPING.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + + let frame = StackFrame::new("com.android.tools.r8.Internal", "zza", 11); + let frames: Vec<_> = cache.remap_frame(&frame).collect(); + + assert_eq!(frames.len(), 1); + assert_eq!( + frames[0], + StackFrame::with_file("com.android.tools.r8.Internal", "foo", 11, "Internal.java") + ); +} + +// ============================================================================= +// AmbiguousInlineFramesStackTrace +// Ambiguity in inline frame chain +// ============================================================================= + +const AMBIGUOUS_INLINE_MAPPING: &str = r#"com.android.tools.r8.R8 -> a.a: + 1:1:void foo(int):42:44 -> a + 1:1:void bar(int, int):32 -> a + 1:1:void baz(int, int):10 -> a +"#; + +#[test] +fn test_ambiguous_inline_frames_mapper() { + let mapper = ProguardMapper::from(AMBIGUOUS_INLINE_MAPPING); + + let input = "\ +Exception in thread \"main\" java.lang.NullPointerException + at a.a.a(SourceFile:1) +"; + + // Inline chain: foo (42-44) -> bar (32) -> baz (10) + let expected = "\ +Exception in thread \"main\" java.lang.NullPointerException + at com.android.tools.r8.R8.foo(R8.java:42) + at com.android.tools.r8.R8.bar(R8.java:32) + at com.android.tools.r8.R8.baz(R8.java:10) +"; + + let actual = mapper.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} + +#[test] +fn test_ambiguous_inline_frames_cache() { + let mapping = ProguardMapping::new(AMBIGUOUS_INLINE_MAPPING.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + + let input = "\ +Exception in thread \"main\" java.lang.NullPointerException + at a.a.a(SourceFile:1) +"; + + let expected = "\ +Exception in thread \"main\" java.lang.NullPointerException + at com.android.tools.r8.R8.foo(R8.java:42) + at com.android.tools.r8.R8.bar(R8.java:32) + at com.android.tools.r8.R8.baz(R8.java:10) +"; + + let actual = cache.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} + +#[test] +fn test_ambiguous_inline_frames_frame_mapper() { + let mapper = ProguardMapper::from(AMBIGUOUS_INLINE_MAPPING); + + let frame = StackFrame::new("a.a", "a", 1); + let frames: Vec<_> = mapper.remap_frame(&frame).collect(); + + assert_eq!(frames.len(), 3); + assert_eq!( + frames[0], + StackFrame::with_file("com.android.tools.r8.R8", "foo", 42, "R8.java") + ); + assert_eq!( + frames[1], + StackFrame::with_file("com.android.tools.r8.R8", "bar", 32, "R8.java") + ); + assert_eq!( + frames[2], + StackFrame::with_file("com.android.tools.r8.R8", "baz", 10, "R8.java") + ); +} + +#[test] +fn test_ambiguous_inline_frames_frame_cache() { + let mapping = ProguardMapping::new(AMBIGUOUS_INLINE_MAPPING.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + + let frame = StackFrame::new("a.a", "a", 1); + let frames: Vec<_> = cache.remap_frame(&frame).collect(); + + assert_eq!(frames.len(), 3); + assert_eq!( + frames[0], + StackFrame::with_file("com.android.tools.r8.R8", "foo", 42, "R8.java") + ); + assert_eq!( + frames[1], + StackFrame::with_file("com.android.tools.r8.R8", "bar", 32, "R8.java") + ); + assert_eq!( + frames[2], + StackFrame::with_file("com.android.tools.r8.R8", "baz", 10, "R8.java") + ); +} + +// ============================================================================= +// AmbiguousMultipleInlineStackTrace +// Multiple ambiguous inline frames from different classes +// ============================================================================= + +const AMBIGUOUS_MULTIPLE_INLINE_MAPPING: &str = r#"com.android.tools.r8.Internal -> com.android.tools.r8.Internal: + 10:10:void some.inlinee1(int):10:10 -> zza + 10:10:void foo(int):10 -> zza + 11:12:void foo(int):11:12 -> zza + 10:10:void some.inlinee2(int, int):20:20 -> zza + 10:10:void foo(int, int):42 -> zza +"#; + +#[test] +fn test_ambiguous_multiple_inline_mapper() { + let mapper = ProguardMapper::from(AMBIGUOUS_MULTIPLE_INLINE_MAPPING); + + let input = "\ +Exception in thread \"main\" java.lang.NullPointerException + at com.android.tools.r8.Internal.zza(SourceFile:10) +"; + + // Line 10 matches two inline chains: + // Chain 1: inlinee1 -> foo(int) + // Chain 2: inlinee2 -> foo(int, int) + // Note: inlinee class is "some", so file synthesizes to "some.java" + let expected = "\ +Exception in thread \"main\" java.lang.NullPointerException + at some.inlinee1(some.java:10) + at com.android.tools.r8.Internal.foo(Internal.java:10) + at some.inlinee2(some.java:20) + at com.android.tools.r8.Internal.foo(Internal.java:42) +"; + + let actual = mapper.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} + +#[test] +fn test_ambiguous_multiple_inline_cache() { + let mapping = ProguardMapping::new(AMBIGUOUS_MULTIPLE_INLINE_MAPPING.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + + let input = "\ +Exception in thread \"main\" java.lang.NullPointerException + at com.android.tools.r8.Internal.zza(SourceFile:10) +"; + + let expected = "\ +Exception in thread \"main\" java.lang.NullPointerException + at some.inlinee1(some.java:10) + at com.android.tools.r8.Internal.foo(Internal.java:10) + at some.inlinee2(some.java:20) + at com.android.tools.r8.Internal.foo(Internal.java:42) +"; + + let actual = cache.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} + +#[test] +fn test_ambiguous_multiple_inline_frame_mapper() { + let mapper = ProguardMapper::from(AMBIGUOUS_MULTIPLE_INLINE_MAPPING); + + let frame = StackFrame::new("com.android.tools.r8.Internal", "zza", 10); + let frames: Vec<_> = mapper.remap_frame(&frame).collect(); + + // Two inline chains = 4 frames total + assert_eq!(frames.len(), 4); +} + +#[test] +fn test_ambiguous_multiple_inline_frame_cache() { + let mapping = ProguardMapping::new(AMBIGUOUS_MULTIPLE_INLINE_MAPPING.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + + let frame = StackFrame::new("com.android.tools.r8.Internal", "zza", 10); + let frames: Vec<_> = cache.remap_frame(&frame).collect(); + + assert_eq!(frames.len(), 4); +} + +// ============================================================================= +// AmbiguousMethodVerboseStackTrace +// Different return types and parameters +// ============================================================================= + +const AMBIGUOUS_VERBOSE_MAPPING: &str = r#"com.android.tools.r8.naming.retrace.Main -> a.a: + com.android.Foo main(java.lang.String[],com.android.Bar) -> a + com.android.Foo main(java.lang.String[]) -> b + void main(com.android.Bar) -> b +"#; + +#[test] +fn test_ambiguous_verbose_mapper() { + let mapper = ProguardMapper::from(AMBIGUOUS_VERBOSE_MAPPING); + + // Method 'a' maps to single method + let input = "\ +Exception in thread \"main\" java.lang.NullPointerException + at a.a.a(SourceFile:0) +"; + + let expected = "\ +Exception in thread \"main\" java.lang.NullPointerException + at com.android.tools.r8.naming.retrace.Main.main(Main.java:0) +"; + + let actual = mapper.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} + +#[test] +fn test_ambiguous_verbose_cache() { + let mapping = ProguardMapping::new(AMBIGUOUS_VERBOSE_MAPPING.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + + let input = "\ +Exception in thread \"main\" java.lang.NullPointerException + at a.a.a(SourceFile:0) +"; + + let expected = "\ +Exception in thread \"main\" java.lang.NullPointerException + at com.android.tools.r8.naming.retrace.Main.main(Main.java:0) +"; + + let actual = cache.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} + +#[test] +fn test_ambiguous_verbose_b_mapper() { + let mapper = ProguardMapper::from(AMBIGUOUS_VERBOSE_MAPPING); + + // Method 'b' maps to two methods (ambiguous) + let input = "\ +Exception in thread \"main\" java.lang.NullPointerException + at a.a.b(SourceFile:0) +"; + + let expected = "\ +Exception in thread \"main\" java.lang.NullPointerException + at com.android.tools.r8.naming.retrace.Main.main(Main.java:0) + at com.android.tools.r8.naming.retrace.Main.main(Main.java:0) +"; + + let actual = mapper.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} + +#[test] +fn test_ambiguous_verbose_b_cache() { + let mapping = ProguardMapping::new(AMBIGUOUS_VERBOSE_MAPPING.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + + let input = "\ +Exception in thread \"main\" java.lang.NullPointerException + at a.a.b(SourceFile:0) +"; + + let expected = "\ +Exception in thread \"main\" java.lang.NullPointerException + at com.android.tools.r8.naming.retrace.Main.main(Main.java:0) + at com.android.tools.r8.naming.retrace.Main.main(Main.java:0) +"; + + let actual = cache.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} + +#[test] +fn test_ambiguous_verbose_frame_mapper() { + let mapper = ProguardMapper::from(AMBIGUOUS_VERBOSE_MAPPING); + + // Method 'a' maps to single method + let frame_a = StackFrame::new("a.a", "a", 0); + let frames_a: Vec<_> = mapper.remap_frame(&frame_a).collect(); + assert_eq!(frames_a.len(), 1); + assert_eq!( + frames_a[0], + StackFrame::with_file( + "com.android.tools.r8.naming.retrace.Main", + "main", + 0, + "Main.java" + ) + ); + + // Method 'b' maps to two methods + let frame_b = StackFrame::new("a.a", "b", 0); + let frames_b: Vec<_> = mapper.remap_frame(&frame_b).collect(); + assert_eq!(frames_b.len(), 2); +} + +#[test] +fn test_ambiguous_verbose_frame_cache() { + let mapping = ProguardMapping::new(AMBIGUOUS_VERBOSE_MAPPING.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + + let frame_b = StackFrame::new("a.a", "b", 0); + let frames_b: Vec<_> = cache.remap_frame(&frame_b).collect(); + assert_eq!(frames_b.len(), 2); +} + +// ============================================================================= +// InlineNoLineAssumeNoInlineAmbiguousStackTrace +// From R8: InlineNoLineAssumeNoInlineAmbiguousStackTrace.java +// Without line info, prefer non-inlined mapping over inlined +// ============================================================================= + +const INLINE_NO_LINE_MAPPING: &str = r#"retrace.Main -> a: + void otherMain(java.lang.String[]) -> foo + 2:2:void method1(java.lang.String):0:0 -> foo + 2:2:void main(java.lang.String[]):0 -> foo +"#; + +#[test] +fn test_inline_no_line_prefer_non_inline_mapper() { + let mapper = ProguardMapper::from(INLINE_NO_LINE_MAPPING); + + // From R8: input has "Unknown Source" - no line number available + let input = "\ +Exception in thread \"main\" java.lang.NullPointerException + at a.foo(Unknown Source) +"; + + // R8 retrace prefers otherMain because it has no line range (not part of inline chain) + let expected = "\ +Exception in thread \"main\" java.lang.NullPointerException + at retrace.Main.otherMain(Main.java) +"; + + let actual = mapper.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} + +#[test] +fn test_inline_no_line_prefer_non_inline_cache() { + let mapping = ProguardMapping::new(INLINE_NO_LINE_MAPPING.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + + let input = "\ +Exception in thread \"main\" java.lang.NullPointerException + at a.foo(Unknown Source) +"; + + let expected = "\ +Exception in thread \"main\" java.lang.NullPointerException + at retrace.Main.otherMain(Main.java) +"; + + let actual = cache.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} + +#[test] +fn test_inline_no_line_prefer_non_inline_frame_mapper() { + let mapper = ProguardMapper::from(INLINE_NO_LINE_MAPPING); + + // Frame with line=0 should prefer non-inlined method + let frame = StackFrame::new("a", "foo", 0); + let frames: Vec<_> = mapper.remap_frame(&frame).collect(); + + assert_eq!(frames.len(), 1); + assert_eq!( + frames[0], + StackFrame::with_file("retrace.Main", "otherMain", 0, "Main.java") + ); +} + +#[test] +fn test_inline_no_line_prefer_non_inline_frame_cache() { + let mapping = ProguardMapping::new(INLINE_NO_LINE_MAPPING.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + + let frame = StackFrame::new("a", "foo", 0); + let frames: Vec<_> = cache.remap_frame(&frame).collect(); + + assert_eq!(frames.len(), 1); + assert_eq!( + frames[0], + StackFrame::with_file("retrace.Main", "otherMain", 0, "Main.java") + ); +} + +#[test] +fn test_inline_no_line_with_line_mapper() { + let mapper = ProguardMapper::from(INLINE_NO_LINE_MAPPING); + + // With line 2, should match the inlined chain + let input = "\ +Exception in thread \"main\" java.lang.NullPointerException + at a.foo(SourceFile:2) +"; + + let expected = "\ +Exception in thread \"main\" java.lang.NullPointerException + at retrace.Main.method1(Main.java:0) + at retrace.Main.main(Main.java:0) +"; + + let actual = mapper.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} + +#[test] +fn test_inline_no_line_with_line_cache() { + let mapping = ProguardMapping::new(INLINE_NO_LINE_MAPPING.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + + let input = "\ +Exception in thread \"main\" java.lang.NullPointerException + at a.foo(SourceFile:2) +"; + + let expected = "\ +Exception in thread \"main\" java.lang.NullPointerException + at retrace.Main.method1(Main.java:0) + at retrace.Main.main(Main.java:0) +"; + + let actual = cache.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} + +#[test] +fn test_inline_no_line_with_line_frame_mapper() { + let mapper = ProguardMapper::from(INLINE_NO_LINE_MAPPING); + + let frame = StackFrame::new("a", "foo", 2); + let frames: Vec<_> = mapper.remap_frame(&frame).collect(); + + assert_eq!(frames.len(), 2); + assert_eq!( + frames[0], + StackFrame::with_file("retrace.Main", "method1", 0, "Main.java") + ); + assert_eq!( + frames[1], + StackFrame::with_file("retrace.Main", "main", 0, "Main.java") + ); +} + +#[test] +fn test_inline_no_line_with_line_frame_cache() { + let mapping = ProguardMapping::new(INLINE_NO_LINE_MAPPING.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + + let frame = StackFrame::new("a", "foo", 2); + let frames: Vec<_> = cache.remap_frame(&frame).collect(); + + assert_eq!(frames.len(), 2); + assert_eq!( + frames[0], + StackFrame::with_file("retrace.Main", "method1", 0, "Main.java") + ); + assert_eq!( + frames[1], + StackFrame::with_file("retrace.Main", "main", 0, "Main.java") + ); +} From 7ae46d0716b8a4d82a4d08e7d6582d87781db032 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 23 Dec 2025 11:53:42 +0100 Subject: [PATCH 02/20] Update tests --- tests/r8-ambiguous.rs | 838 +++++++----------------------------------- 1 file changed, 132 insertions(+), 706 deletions(-) diff --git a/tests/r8-ambiguous.rs b/tests/r8-ambiguous.rs index a539c5f..579f848 100644 --- a/tests/r8-ambiguous.rs +++ b/tests/r8-ambiguous.rs @@ -1,918 +1,344 @@ -//! Tests for ambiguous method retracing functionality. +//! Tests for R8 ambiguous method retracing functionality. //! //! These tests are based on the R8 retrace test suite from: //! src/test/java/com/android/tools/r8/retrace/stacktraces/ -//! -//! When multiple original methods map to the same obfuscated name, -//! the retrace should return all possible alternatives. -use proguard::{ProguardCache, ProguardMapper, ProguardMapping, StackFrame}; +use proguard::{ProguardCache, ProguardMapper, ProguardMapping}; // ============================================================================= // AmbiguousStackTrace -// Multiple methods (foo and bar) map to the same obfuscated name 'a' // ============================================================================= -const AMBIGUOUS_MAPPING: &str = r#"com.android.tools.r8.R8 -> a.a: +const AMBIGUOUS_STACKTRACE_MAPPING: &str = "\ +com.android.tools.r8.R8 -> a.a: void foo(int) -> a void bar(int, int) -> a -"#; - -#[test] -fn test_ambiguous_methods_mapper() { - let mapper = ProguardMapper::from(AMBIGUOUS_MAPPING); - - let input = "\ -Exception in thread \"main\" java.lang.NullPointerException - at a.a.a(SourceFile:0) -"; - - // Both foo and bar should appear as alternatives - let expected = "\ -Exception in thread \"main\" java.lang.NullPointerException - at com.android.tools.r8.R8.foo(R8.java:0) - at com.android.tools.r8.R8.bar(R8.java:0) "; - let actual = mapper.remap_stacktrace(input).unwrap(); - assert_eq!(actual.trim(), expected.trim()); -} - #[test] -fn test_ambiguous_methods_cache() { - let mapping = ProguardMapping::new(AMBIGUOUS_MAPPING.as_bytes()); - let mut buf = Vec::new(); - ProguardCache::write(&mapping, &mut buf).unwrap(); - let cache = ProguardCache::parse(&buf).unwrap(); - +fn test_ambiguous_stacktrace() { let input = "\ -Exception in thread \"main\" java.lang.NullPointerException - at a.a.a(SourceFile:0) +com.android.tools.r8.CompilationException: foo[parens](Source:3) + at a.a.a(Unknown Source) + at a.a.a(Unknown Source) + at com.android.tools.r8.R8.main(Unknown Source) +Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3) + at a.a.a(Unknown Source) + ... 42 more "; let expected = "\ -Exception in thread \"main\" java.lang.NullPointerException +com.android.tools.r8.CompilationException: foo[parens](Source:3) at com.android.tools.r8.R8.foo(R8.java:0) at com.android.tools.r8.R8.bar(R8.java:0) -"; - - let actual = cache.remap_stacktrace(input).unwrap(); - assert_eq!(actual.trim(), expected.trim()); -} - -#[test] -fn test_ambiguous_methods_frame_mapper() { - let mapper = ProguardMapper::from(AMBIGUOUS_MAPPING); - - let frame = StackFrame::new("a.a", "a", 0); - let frames: Vec<_> = mapper.remap_frame(&frame).collect(); - - assert_eq!(frames.len(), 2); - assert_eq!( - frames[0], - StackFrame::with_file("com.android.tools.r8.R8", "foo", 0, "R8.java") - ); - assert_eq!( - frames[1], - StackFrame::with_file("com.android.tools.r8.R8", "bar", 0, "R8.java") - ); -} - -#[test] -fn test_ambiguous_methods_frame_cache() { - let mapping = ProguardMapping::new(AMBIGUOUS_MAPPING.as_bytes()); - let mut buf = Vec::new(); - ProguardCache::write(&mapping, &mut buf).unwrap(); - let cache = ProguardCache::parse(&buf).unwrap(); - - let frame = StackFrame::new("a.a", "a", 0); - let frames: Vec<_> = cache.remap_frame(&frame).collect(); - - assert_eq!(frames.len(), 2); - assert_eq!( - frames[0], - StackFrame::with_file("com.android.tools.r8.R8", "foo", 0, "R8.java") - ); - assert_eq!( - frames[1], - StackFrame::with_file("com.android.tools.r8.R8", "bar", 0, "R8.java") - ); -} - -// ============================================================================= -// AmbiguousMissingLineStackTrace -// Ambiguous methods with line numbers that don't match any range -// ============================================================================= - -#[test] -fn test_ambiguous_with_line_number_mapper() { - let mapper = ProguardMapper::from(AMBIGUOUS_MAPPING); - - let input = "\ -Exception in thread \"main\" java.lang.NullPointerException - at a.a.a(SourceFile:7) -"; - - // Line number 7 doesn't match any specific range, but methods have no line ranges - // so both methods should still be returned (line preserved as 0) - let expected = "\ -Exception in thread \"main\" java.lang.NullPointerException at com.android.tools.r8.R8.foo(R8.java:0) at com.android.tools.r8.R8.bar(R8.java:0) -"; - - let actual = mapper.remap_stacktrace(input).unwrap(); - assert_eq!(actual.trim(), expected.trim()); -} - -#[test] -fn test_ambiguous_with_line_number_cache() { - let mapping = ProguardMapping::new(AMBIGUOUS_MAPPING.as_bytes()); - let mut buf = Vec::new(); - ProguardCache::write(&mapping, &mut buf).unwrap(); - let cache = ProguardCache::parse(&buf).unwrap(); - - let input = "\ -Exception in thread \"main\" java.lang.NullPointerException - at a.a.a(SourceFile:7) -"; - - let expected = "\ -Exception in thread \"main\" java.lang.NullPointerException + at com.android.tools.r8.R8.main(Unknown Source) +Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3) at com.android.tools.r8.R8.foo(R8.java:0) at com.android.tools.r8.R8.bar(R8.java:0) + ... 42 more "; - let actual = cache.remap_stacktrace(input).unwrap(); - assert_eq!(actual.trim(), expected.trim()); -} - -#[test] -fn test_ambiguous_with_line_number_frame_mapper() { - let mapper = ProguardMapper::from(AMBIGUOUS_MAPPING); - - let frame = StackFrame::new("a.a", "a", 7); - let frames: Vec<_> = mapper.remap_frame(&frame).collect(); - - assert_eq!(frames.len(), 2); -} - -#[test] -fn test_ambiguous_with_line_number_frame_cache() { - let mapping = ProguardMapping::new(AMBIGUOUS_MAPPING.as_bytes()); - let mut buf = Vec::new(); - ProguardCache::write(&mapping, &mut buf).unwrap(); - let cache = ProguardCache::parse(&buf).unwrap(); - - let frame = StackFrame::new("a.a", "a", 7); - let frames: Vec<_> = cache.remap_frame(&frame).collect(); - - assert_eq!(frames.len(), 2); -} - -// ============================================================================= -// AmbiguousWithSignatureStackTrace -// Multiple overloaded methods with different signatures -// From R8: AmbiguousWithSignatureStackTrace.java -// ============================================================================= - -const AMBIGUOUS_SIGNATURE_MAPPING: &str = r#"com.android.tools.r8.Internal -> com.android.tools.r8.Internal: - 10:10:void foo(int):10:10 -> zza - 11:11:void foo(int, int):11:11 -> zza - 12:12:void foo(int, boolean):12:12 -> zza - 13:13:boolean foo(int, int):13:13 -> zza -"#; - -#[test] -fn test_ambiguous_signature_no_line_mapper() { - let mapper = ProguardMapper::from(AMBIGUOUS_SIGNATURE_MAPPING); - - // From R8: input has "Unknown" - no line number available - let input = "\ -java.lang.IndexOutOfBoundsException - at java.util.ArrayList.get(ArrayList.java:411) - at com.android.tools.r8.Internal.zza(Unknown) -"; - - // R8 retrace shows all 4 overloads as ambiguous alternatives - let expected = "\ -java.lang.IndexOutOfBoundsException - at java.util.ArrayList.get(ArrayList.java:411) - at com.android.tools.r8.Internal.foo(Internal.java:0) -"; - - let actual = mapper.remap_stacktrace(input).unwrap(); - assert_eq!(actual.trim(), expected.trim()); -} - -#[test] -fn test_ambiguous_signature_no_line_cache() { - let mapping = ProguardMapping::new(AMBIGUOUS_SIGNATURE_MAPPING.as_bytes()); - let mut buf = Vec::new(); - ProguardCache::write(&mapping, &mut buf).unwrap(); - let cache = ProguardCache::parse(&buf).unwrap(); - - let input = "\ -java.lang.IndexOutOfBoundsException - at java.util.ArrayList.get(ArrayList.java:411) - at com.android.tools.r8.Internal.zza(Unknown) -"; - - let expected = "\ -java.lang.IndexOutOfBoundsException - at java.util.ArrayList.get(ArrayList.java:411) - at com.android.tools.r8.Internal.foo(Internal.java:0) -"; - - let actual = cache.remap_stacktrace(input).unwrap(); - assert_eq!(actual.trim(), expected.trim()); -} - -#[test] -fn test_ambiguous_signature_with_line_mapper() { - let mapper = ProguardMapper::from(AMBIGUOUS_SIGNATURE_MAPPING); - - let input = "\ -java.lang.IndexOutOfBoundsException - at java.util.ArrayList.get(ArrayList.java:411) - at com.android.tools.r8.Internal.zza(SourceFile:10) -"; - - // Line 10 disambiguates to single method - let expected = "\ -java.lang.IndexOutOfBoundsException - at java.util.ArrayList.get(ArrayList.java:411) - at com.android.tools.r8.Internal.foo(Internal.java:10) -"; - + let mapper = ProguardMapper::from(AMBIGUOUS_STACKTRACE_MAPPING); let actual = mapper.remap_stacktrace(input).unwrap(); assert_eq!(actual.trim(), expected.trim()); -} -#[test] -fn test_ambiguous_signature_with_line_cache() { - let mapping = ProguardMapping::new(AMBIGUOUS_SIGNATURE_MAPPING.as_bytes()); + let mapping = ProguardMapping::new(AMBIGUOUS_STACKTRACE_MAPPING.as_bytes()); let mut buf = Vec::new(); ProguardCache::write(&mapping, &mut buf).unwrap(); let cache = ProguardCache::parse(&buf).unwrap(); - - let input = "\ -java.lang.IndexOutOfBoundsException - at java.util.ArrayList.get(ArrayList.java:411) - at com.android.tools.r8.Internal.zza(SourceFile:10) -"; - - let expected = "\ -java.lang.IndexOutOfBoundsException - at java.util.ArrayList.get(ArrayList.java:411) - at com.android.tools.r8.Internal.foo(Internal.java:10) -"; + cache.test(); let actual = cache.remap_stacktrace(input).unwrap(); assert_eq!(actual.trim(), expected.trim()); } -#[test] -fn test_ambiguous_signature_with_line_frame_mapper() { - let mapper = ProguardMapper::from(AMBIGUOUS_SIGNATURE_MAPPING); - - let frame = StackFrame::new("com.android.tools.r8.Internal", "zza", 10); - let frames: Vec<_> = mapper.remap_frame(&frame).collect(); - - assert_eq!(frames.len(), 1); - assert_eq!( - frames[0], - StackFrame::with_file("com.android.tools.r8.Internal", "foo", 10, "Internal.java") - ); -} - -#[test] -fn test_ambiguous_signature_with_line_frame_cache() { - let mapping = ProguardMapping::new(AMBIGUOUS_SIGNATURE_MAPPING.as_bytes()); - let mut buf = Vec::new(); - ProguardCache::write(&mapping, &mut buf).unwrap(); - let cache = ProguardCache::parse(&buf).unwrap(); - - let frame = StackFrame::new("com.android.tools.r8.Internal", "zza", 11); - let frames: Vec<_> = cache.remap_frame(&frame).collect(); - - assert_eq!(frames.len(), 1); - assert_eq!( - frames[0], - StackFrame::with_file("com.android.tools.r8.Internal", "foo", 11, "Internal.java") - ); -} - // ============================================================================= -// AmbiguousWithMultipleLineMappingsStackTrace -// Same method with multiple line ranges -// From R8: AmbiguousWithMultipleLineMappingsStackTrace.java +// AmbiguousMissingLineStackTrace // ============================================================================= -const AMBIGUOUS_MULTIPLE_LINES_MAPPING: &str = r#"com.android.tools.r8.Internal -> com.android.tools.r8.Internal: - 10:10:void foo(int):10:10 -> zza - 11:11:void foo(int):11:11 -> zza - 12:12:void foo(int):12:12 -> zza -"#; - -#[test] -fn test_ambiguous_multiple_lines_no_line_mapper() { - let mapper = ProguardMapper::from(AMBIGUOUS_MULTIPLE_LINES_MAPPING); - - // From R8: input has "Unknown" - no line number available - let input = "\ -java.lang.IndexOutOfBoundsException - at java.util.ArrayList.get(ArrayList.java:411) - at com.android.tools.r8.Internal.zza(Unknown) -"; - - // All 3 map to same method signature, R8 shows one result with no line - let expected = "\ -java.lang.IndexOutOfBoundsException - at java.util.ArrayList.get(ArrayList.java:411) - at com.android.tools.r8.Internal.foo(Internal.java) -"; - - let actual = mapper.remap_stacktrace(input).unwrap(); - assert_eq!(actual.trim(), expected.trim()); -} - -#[test] -fn test_ambiguous_multiple_lines_no_line_cache() { - let mapping = ProguardMapping::new(AMBIGUOUS_MULTIPLE_LINES_MAPPING.as_bytes()); - let mut buf = Vec::new(); - ProguardCache::write(&mapping, &mut buf).unwrap(); - let cache = ProguardCache::parse(&buf).unwrap(); - - let input = "\ -java.lang.IndexOutOfBoundsException - at java.util.ArrayList.get(ArrayList.java:411) - at com.android.tools.r8.Internal.zza(Unknown) -"; - - let expected = "\ -java.lang.IndexOutOfBoundsException - at java.util.ArrayList.get(ArrayList.java:411) - at com.android.tools.r8.Internal.foo(Internal.java) +const AMBIGUOUS_MISSING_LINE_MAPPING: &str = "\ +com.android.tools.r8.R8 -> a.a: + void foo(int) -> a + void bar(int, int) -> a "; - let actual = cache.remap_stacktrace(input).unwrap(); - assert_eq!(actual.trim(), expected.trim()); -} - #[test] -fn test_ambiguous_multiple_lines_with_line_mapper() { - let mapper = ProguardMapper::from(AMBIGUOUS_MULTIPLE_LINES_MAPPING); - +fn test_ambiguous_missing_line_stacktrace() { let input = "\ -java.lang.IndexOutOfBoundsException - at java.util.ArrayList.get(ArrayList.java:411) - at com.android.tools.r8.Internal.zza(SourceFile:10) +com.android.tools.r8.CompilationException: foo[parens](Source:3) + at a.a.a(Unknown Source:7) + at a.a.a(Unknown Source:8) + at com.android.tools.r8.R8.main(Unknown Source) +Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3) + at a.a.a(Unknown Source:9) + ... 42 more "; let expected = "\ -java.lang.IndexOutOfBoundsException - at java.util.ArrayList.get(ArrayList.java:411) - at com.android.tools.r8.Internal.foo(Internal.java:10) +com.android.tools.r8.CompilationException: foo[parens](Source:3) + at com.android.tools.r8.R8.foo(R8.java:0) + at com.android.tools.r8.R8.bar(R8.java:0) + at com.android.tools.r8.R8.foo(R8.java:0) + at com.android.tools.r8.R8.bar(R8.java:0) + at com.android.tools.r8.R8.main(Unknown Source) +Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3) + at com.android.tools.r8.R8.foo(R8.java:0) + at com.android.tools.r8.R8.bar(R8.java:0) + ... 42 more "; + let mapper = ProguardMapper::from(AMBIGUOUS_MISSING_LINE_MAPPING); let actual = mapper.remap_stacktrace(input).unwrap(); assert_eq!(actual.trim(), expected.trim()); -} -#[test] -fn test_ambiguous_multiple_lines_with_line_cache() { - let mapping = ProguardMapping::new(AMBIGUOUS_MULTIPLE_LINES_MAPPING.as_bytes()); + let mapping = ProguardMapping::new(AMBIGUOUS_MISSING_LINE_MAPPING.as_bytes()); let mut buf = Vec::new(); ProguardCache::write(&mapping, &mut buf).unwrap(); let cache = ProguardCache::parse(&buf).unwrap(); - - let input = "\ -java.lang.IndexOutOfBoundsException - at java.util.ArrayList.get(ArrayList.java:411) - at com.android.tools.r8.Internal.zza(SourceFile:10) -"; - - let expected = "\ -java.lang.IndexOutOfBoundsException - at java.util.ArrayList.get(ArrayList.java:411) - at com.android.tools.r8.Internal.foo(Internal.java:10) -"; + cache.test(); let actual = cache.remap_stacktrace(input).unwrap(); assert_eq!(actual.trim(), expected.trim()); } -#[test] -fn test_ambiguous_multiple_lines_with_line_frame_mapper() { - let mapper = ProguardMapper::from(AMBIGUOUS_MULTIPLE_LINES_MAPPING); - - let frame = StackFrame::new("com.android.tools.r8.Internal", "zza", 10); - let frames: Vec<_> = mapper.remap_frame(&frame).collect(); - - assert_eq!(frames.len(), 1); - assert_eq!( - frames[0], - StackFrame::with_file("com.android.tools.r8.Internal", "foo", 10, "Internal.java") - ); -} - -#[test] -fn test_ambiguous_multiple_lines_with_line_frame_cache() { - let mapping = ProguardMapping::new(AMBIGUOUS_MULTIPLE_LINES_MAPPING.as_bytes()); - let mut buf = Vec::new(); - ProguardCache::write(&mapping, &mut buf).unwrap(); - let cache = ProguardCache::parse(&buf).unwrap(); - - let frame = StackFrame::new("com.android.tools.r8.Internal", "zza", 11); - let frames: Vec<_> = cache.remap_frame(&frame).collect(); - - assert_eq!(frames.len(), 1); - assert_eq!( - frames[0], - StackFrame::with_file("com.android.tools.r8.Internal", "foo", 11, "Internal.java") - ); -} - // ============================================================================= // AmbiguousInlineFramesStackTrace -// Ambiguity in inline frame chain // ============================================================================= -const AMBIGUOUS_INLINE_MAPPING: &str = r#"com.android.tools.r8.R8 -> a.a: +const AMBIGUOUS_INLINE_FRAMES_MAPPING: &str = "\ +com.android.tools.r8.R8 -> a.a: 1:1:void foo(int):42:44 -> a 1:1:void bar(int, int):32 -> a 1:1:void baz(int, int):10 -> a -"#; +"; #[test] -fn test_ambiguous_inline_frames_mapper() { - let mapper = ProguardMapper::from(AMBIGUOUS_INLINE_MAPPING); - +fn test_ambiguous_inline_frames_stacktrace() { let input = "\ -Exception in thread \"main\" java.lang.NullPointerException - at a.a.a(SourceFile:1) +com.android.tools.r8.CompilationException: + at a.a.a(Unknown Source:1) "; - // Inline chain: foo (42-44) -> bar (32) -> baz (10) let expected = "\ -Exception in thread \"main\" java.lang.NullPointerException +com.android.tools.r8.CompilationException: at com.android.tools.r8.R8.foo(R8.java:42) at com.android.tools.r8.R8.bar(R8.java:32) at com.android.tools.r8.R8.baz(R8.java:10) "; + let mapper = ProguardMapper::from(AMBIGUOUS_INLINE_FRAMES_MAPPING); let actual = mapper.remap_stacktrace(input).unwrap(); assert_eq!(actual.trim(), expected.trim()); -} -#[test] -fn test_ambiguous_inline_frames_cache() { - let mapping = ProguardMapping::new(AMBIGUOUS_INLINE_MAPPING.as_bytes()); + let mapping = ProguardMapping::new(AMBIGUOUS_INLINE_FRAMES_MAPPING.as_bytes()); let mut buf = Vec::new(); ProguardCache::write(&mapping, &mut buf).unwrap(); let cache = ProguardCache::parse(&buf).unwrap(); - - let input = "\ -Exception in thread \"main\" java.lang.NullPointerException - at a.a.a(SourceFile:1) -"; - - let expected = "\ -Exception in thread \"main\" java.lang.NullPointerException - at com.android.tools.r8.R8.foo(R8.java:42) - at com.android.tools.r8.R8.bar(R8.java:32) - at com.android.tools.r8.R8.baz(R8.java:10) -"; + cache.test(); let actual = cache.remap_stacktrace(input).unwrap(); assert_eq!(actual.trim(), expected.trim()); } -#[test] -fn test_ambiguous_inline_frames_frame_mapper() { - let mapper = ProguardMapper::from(AMBIGUOUS_INLINE_MAPPING); - - let frame = StackFrame::new("a.a", "a", 1); - let frames: Vec<_> = mapper.remap_frame(&frame).collect(); - - assert_eq!(frames.len(), 3); - assert_eq!( - frames[0], - StackFrame::with_file("com.android.tools.r8.R8", "foo", 42, "R8.java") - ); - assert_eq!( - frames[1], - StackFrame::with_file("com.android.tools.r8.R8", "bar", 32, "R8.java") - ); - assert_eq!( - frames[2], - StackFrame::with_file("com.android.tools.r8.R8", "baz", 10, "R8.java") - ); -} - -#[test] -fn test_ambiguous_inline_frames_frame_cache() { - let mapping = ProguardMapping::new(AMBIGUOUS_INLINE_MAPPING.as_bytes()); - let mut buf = Vec::new(); - ProguardCache::write(&mapping, &mut buf).unwrap(); - let cache = ProguardCache::parse(&buf).unwrap(); - - let frame = StackFrame::new("a.a", "a", 1); - let frames: Vec<_> = cache.remap_frame(&frame).collect(); - - assert_eq!(frames.len(), 3); - assert_eq!( - frames[0], - StackFrame::with_file("com.android.tools.r8.R8", "foo", 42, "R8.java") - ); - assert_eq!( - frames[1], - StackFrame::with_file("com.android.tools.r8.R8", "bar", 32, "R8.java") - ); - assert_eq!( - frames[2], - StackFrame::with_file("com.android.tools.r8.R8", "baz", 10, "R8.java") - ); -} - // ============================================================================= // AmbiguousMultipleInlineStackTrace -// Multiple ambiguous inline frames from different classes // ============================================================================= -const AMBIGUOUS_MULTIPLE_INLINE_MAPPING: &str = r#"com.android.tools.r8.Internal -> com.android.tools.r8.Internal: +const AMBIGUOUS_MULTIPLE_INLINE_MAPPING: &str = "\ +com.android.tools.r8.Internal -> com.android.tools.r8.Internal: 10:10:void some.inlinee1(int):10:10 -> zza 10:10:void foo(int):10 -> zza 11:12:void foo(int):11:12 -> zza 10:10:void some.inlinee2(int, int):20:20 -> zza 10:10:void foo(int, int):42 -> zza -"#; +"; #[test] -fn test_ambiguous_multiple_inline_mapper() { - let mapper = ProguardMapper::from(AMBIGUOUS_MULTIPLE_INLINE_MAPPING); - +fn test_ambiguous_multiple_inline_stacktrace() { let input = "\ -Exception in thread \"main\" java.lang.NullPointerException +java.lang.IndexOutOfBoundsException at com.android.tools.r8.Internal.zza(SourceFile:10) "; - // Line 10 matches two inline chains: - // Chain 1: inlinee1 -> foo(int) - // Chain 2: inlinee2 -> foo(int, int) - // Note: inlinee class is "some", so file synthesizes to "some.java" let expected = "\ -Exception in thread \"main\" java.lang.NullPointerException +java.lang.IndexOutOfBoundsException at some.inlinee1(some.java:10) at com.android.tools.r8.Internal.foo(Internal.java:10) at some.inlinee2(some.java:20) at com.android.tools.r8.Internal.foo(Internal.java:42) "; + let mapper = ProguardMapper::from(AMBIGUOUS_MULTIPLE_INLINE_MAPPING); let actual = mapper.remap_stacktrace(input).unwrap(); assert_eq!(actual.trim(), expected.trim()); -} -#[test] -fn test_ambiguous_multiple_inline_cache() { let mapping = ProguardMapping::new(AMBIGUOUS_MULTIPLE_INLINE_MAPPING.as_bytes()); let mut buf = Vec::new(); ProguardCache::write(&mapping, &mut buf).unwrap(); let cache = ProguardCache::parse(&buf).unwrap(); - - let input = "\ -Exception in thread \"main\" java.lang.NullPointerException - at com.android.tools.r8.Internal.zza(SourceFile:10) -"; - - let expected = "\ -Exception in thread \"main\" java.lang.NullPointerException - at some.inlinee1(some.java:10) - at com.android.tools.r8.Internal.foo(Internal.java:10) - at some.inlinee2(some.java:20) - at com.android.tools.r8.Internal.foo(Internal.java:42) -"; + cache.test(); let actual = cache.remap_stacktrace(input).unwrap(); assert_eq!(actual.trim(), expected.trim()); } -#[test] -fn test_ambiguous_multiple_inline_frame_mapper() { - let mapper = ProguardMapper::from(AMBIGUOUS_MULTIPLE_INLINE_MAPPING); - - let frame = StackFrame::new("com.android.tools.r8.Internal", "zza", 10); - let frames: Vec<_> = mapper.remap_frame(&frame).collect(); - - // Two inline chains = 4 frames total - assert_eq!(frames.len(), 4); -} - -#[test] -fn test_ambiguous_multiple_inline_frame_cache() { - let mapping = ProguardMapping::new(AMBIGUOUS_MULTIPLE_INLINE_MAPPING.as_bytes()); - let mut buf = Vec::new(); - ProguardCache::write(&mapping, &mut buf).unwrap(); - let cache = ProguardCache::parse(&buf).unwrap(); - - let frame = StackFrame::new("com.android.tools.r8.Internal", "zza", 10); - let frames: Vec<_> = cache.remap_frame(&frame).collect(); - - assert_eq!(frames.len(), 4); -} - // ============================================================================= -// AmbiguousMethodVerboseStackTrace -// Different return types and parameters +// AmbiguousMethodVerboseStackTrace (non-verbose retrace output) // ============================================================================= -const AMBIGUOUS_VERBOSE_MAPPING: &str = r#"com.android.tools.r8.naming.retrace.Main -> a.a: +const AMBIGUOUS_METHOD_VERBOSE_MAPPING: &str = "\ +com.android.tools.r8.naming.retrace.Main -> a.a: com.android.Foo main(java.lang.String[],com.android.Bar) -> a com.android.Foo main(java.lang.String[]) -> b void main(com.android.Bar) -> b -"#; +"; #[test] -fn test_ambiguous_verbose_mapper() { - let mapper = ProguardMapper::from(AMBIGUOUS_VERBOSE_MAPPING); - - // Method 'a' maps to single method +fn test_ambiguous_method_verbose_stacktrace() { let input = "\ Exception in thread \"main\" java.lang.NullPointerException - at a.a.a(SourceFile:0) + at a.a.c(Foo.java) + at a.a.b(Bar.java) + at a.a.a(Baz.java) "; let expected = "\ Exception in thread \"main\" java.lang.NullPointerException + at com.android.tools.r8.naming.retrace.Main.c(Main.java:0) + at com.android.tools.r8.naming.retrace.Main.main(Main.java:0) at com.android.tools.r8.naming.retrace.Main.main(Main.java:0) "; + let mapper = ProguardMapper::from(AMBIGUOUS_METHOD_VERBOSE_MAPPING); let actual = mapper.remap_stacktrace(input).unwrap(); assert_eq!(actual.trim(), expected.trim()); -} -#[test] -fn test_ambiguous_verbose_cache() { - let mapping = ProguardMapping::new(AMBIGUOUS_VERBOSE_MAPPING.as_bytes()); + let mapping = ProguardMapping::new(AMBIGUOUS_METHOD_VERBOSE_MAPPING.as_bytes()); let mut buf = Vec::new(); ProguardCache::write(&mapping, &mut buf).unwrap(); let cache = ProguardCache::parse(&buf).unwrap(); - - let input = "\ -Exception in thread \"main\" java.lang.NullPointerException - at a.a.a(SourceFile:0) -"; - - let expected = "\ -Exception in thread \"main\" java.lang.NullPointerException - at com.android.tools.r8.naming.retrace.Main.main(Main.java:0) -"; + cache.test(); let actual = cache.remap_stacktrace(input).unwrap(); assert_eq!(actual.trim(), expected.trim()); } -#[test] -fn test_ambiguous_verbose_b_mapper() { - let mapper = ProguardMapper::from(AMBIGUOUS_VERBOSE_MAPPING); - - // Method 'b' maps to two methods (ambiguous) - let input = "\ -Exception in thread \"main\" java.lang.NullPointerException - at a.a.b(SourceFile:0) -"; +// ============================================================================= +// AmbiguousWithMultipleLineMappingsStackTrace +// ============================================================================= - let expected = "\ -Exception in thread \"main\" java.lang.NullPointerException - at com.android.tools.r8.naming.retrace.Main.main(Main.java:0) - at com.android.tools.r8.naming.retrace.Main.main(Main.java:0) +const AMBIGUOUS_WITH_MULTIPLE_LINE_MAPPINGS_MAPPING: &str = "\ +com.android.tools.r8.Internal -> com.android.tools.r8.Internal: + 10:10:void foo(int):10:10 -> zza + 11:11:void foo(int):11:11 -> zza + 12:12:void foo(int):12:12 -> zza "; - let actual = mapper.remap_stacktrace(input).unwrap(); - assert_eq!(actual.trim(), expected.trim()); -} - #[test] -fn test_ambiguous_verbose_b_cache() { - let mapping = ProguardMapping::new(AMBIGUOUS_VERBOSE_MAPPING.as_bytes()); - let mut buf = Vec::new(); - ProguardCache::write(&mapping, &mut buf).unwrap(); - let cache = ProguardCache::parse(&buf).unwrap(); - +fn test_ambiguous_with_multiple_line_mappings_stacktrace() { let input = "\ -Exception in thread \"main\" java.lang.NullPointerException - at a.a.b(SourceFile:0) +java.lang.IndexOutOfBoundsException + at java.util.ArrayList.get(ArrayList.java:411) + at com.android.tools.r8.Internal.zza(Unknown) "; let expected = "\ -Exception in thread \"main\" java.lang.NullPointerException - at com.android.tools.r8.naming.retrace.Main.main(Main.java:0) - at com.android.tools.r8.naming.retrace.Main.main(Main.java:0) +java.lang.IndexOutOfBoundsException + at java.util.ArrayList.get(ArrayList.java:411) + at com.android.tools.r8.Internal.foo(Internal.java:0) "; - let actual = cache.remap_stacktrace(input).unwrap(); + let mapper = ProguardMapper::from(AMBIGUOUS_WITH_MULTIPLE_LINE_MAPPINGS_MAPPING); + let actual = mapper.remap_stacktrace(input).unwrap(); assert_eq!(actual.trim(), expected.trim()); -} -#[test] -fn test_ambiguous_verbose_frame_mapper() { - let mapper = ProguardMapper::from(AMBIGUOUS_VERBOSE_MAPPING); - - // Method 'a' maps to single method - let frame_a = StackFrame::new("a.a", "a", 0); - let frames_a: Vec<_> = mapper.remap_frame(&frame_a).collect(); - assert_eq!(frames_a.len(), 1); - assert_eq!( - frames_a[0], - StackFrame::with_file( - "com.android.tools.r8.naming.retrace.Main", - "main", - 0, - "Main.java" - ) - ); - - // Method 'b' maps to two methods - let frame_b = StackFrame::new("a.a", "b", 0); - let frames_b: Vec<_> = mapper.remap_frame(&frame_b).collect(); - assert_eq!(frames_b.len(), 2); -} - -#[test] -fn test_ambiguous_verbose_frame_cache() { - let mapping = ProguardMapping::new(AMBIGUOUS_VERBOSE_MAPPING.as_bytes()); + let mapping = ProguardMapping::new(AMBIGUOUS_WITH_MULTIPLE_LINE_MAPPINGS_MAPPING.as_bytes()); let mut buf = Vec::new(); ProguardCache::write(&mapping, &mut buf).unwrap(); let cache = ProguardCache::parse(&buf).unwrap(); + cache.test(); - let frame_b = StackFrame::new("a.a", "b", 0); - let frames_b: Vec<_> = cache.remap_frame(&frame_b).collect(); - assert_eq!(frames_b.len(), 2); + let actual = cache.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); } // ============================================================================= -// InlineNoLineAssumeNoInlineAmbiguousStackTrace -// From R8: InlineNoLineAssumeNoInlineAmbiguousStackTrace.java -// Without line info, prefer non-inlined mapping over inlined +// AmbiguousWithSignatureStackTrace (non-verbose retrace output) // ============================================================================= -const INLINE_NO_LINE_MAPPING: &str = r#"retrace.Main -> a: - void otherMain(java.lang.String[]) -> foo - 2:2:void method1(java.lang.String):0:0 -> foo - 2:2:void main(java.lang.String[]):0 -> foo -"#; +const AMBIGUOUS_WITH_SIGNATURE_MAPPING: &str = "\ +com.android.tools.r8.Internal -> com.android.tools.r8.Internal: + 10:10:void foo(int):10:10 -> zza + 11:11:void foo(int, int):11:11 -> zza + 12:12:void foo(int, boolean):12:12 -> zza + 13:13:boolean foo(int, int):13:13 -> zza +"; #[test] -fn test_inline_no_line_prefer_non_inline_mapper() { - let mapper = ProguardMapper::from(INLINE_NO_LINE_MAPPING); - - // From R8: input has "Unknown Source" - no line number available +fn test_ambiguous_with_signature_stacktrace() { let input = "\ -Exception in thread \"main\" java.lang.NullPointerException - at a.foo(Unknown Source) +java.lang.IndexOutOfBoundsException + at java.util.ArrayList.get(ArrayList.java:411) + at com.android.tools.r8.Internal.zza(Unknown) "; - // R8 retrace prefers otherMain because it has no line range (not part of inline chain) let expected = "\ -Exception in thread \"main\" java.lang.NullPointerException - at retrace.Main.otherMain(Main.java) +java.lang.IndexOutOfBoundsException + at java.util.ArrayList.get(ArrayList.java:411) + at com.android.tools.r8.Internal.foo(Internal.java:0) "; + let mapper = ProguardMapper::from(AMBIGUOUS_WITH_SIGNATURE_MAPPING); let actual = mapper.remap_stacktrace(input).unwrap(); assert_eq!(actual.trim(), expected.trim()); -} -#[test] -fn test_inline_no_line_prefer_non_inline_cache() { - let mapping = ProguardMapping::new(INLINE_NO_LINE_MAPPING.as_bytes()); + let mapping = ProguardMapping::new(AMBIGUOUS_WITH_SIGNATURE_MAPPING.as_bytes()); let mut buf = Vec::new(); ProguardCache::write(&mapping, &mut buf).unwrap(); let cache = ProguardCache::parse(&buf).unwrap(); - - let input = "\ -Exception in thread \"main\" java.lang.NullPointerException - at a.foo(Unknown Source) -"; - - let expected = "\ -Exception in thread \"main\" java.lang.NullPointerException - at retrace.Main.otherMain(Main.java) -"; + cache.test(); let actual = cache.remap_stacktrace(input).unwrap(); assert_eq!(actual.trim(), expected.trim()); } -#[test] -fn test_inline_no_line_prefer_non_inline_frame_mapper() { - let mapper = ProguardMapper::from(INLINE_NO_LINE_MAPPING); - - // Frame with line=0 should prefer non-inlined method - let frame = StackFrame::new("a", "foo", 0); - let frames: Vec<_> = mapper.remap_frame(&frame).collect(); - - assert_eq!(frames.len(), 1); - assert_eq!( - frames[0], - StackFrame::with_file("retrace.Main", "otherMain", 0, "Main.java") - ); -} - -#[test] -fn test_inline_no_line_prefer_non_inline_frame_cache() { - let mapping = ProguardMapping::new(INLINE_NO_LINE_MAPPING.as_bytes()); - let mut buf = Vec::new(); - ProguardCache::write(&mapping, &mut buf).unwrap(); - let cache = ProguardCache::parse(&buf).unwrap(); - - let frame = StackFrame::new("a", "foo", 0); - let frames: Vec<_> = cache.remap_frame(&frame).collect(); +// ============================================================================= +// InlineNoLineAssumeNoInlineAmbiguousStackTrace +// ============================================================================= - assert_eq!(frames.len(), 1); - assert_eq!( - frames[0], - StackFrame::with_file("retrace.Main", "otherMain", 0, "Main.java") - ); -} +const INLINE_NO_LINE_ASSUME_NO_INLINE_AMBIGUOUS_MAPPING: &str = "\ +retrace.Main -> a: + void otherMain(java.lang.String[]) -> foo + 2:2:void method1(java.lang.String):0:0 -> foo + 2:2:void main(java.lang.String[]):0 -> foo +"; #[test] -fn test_inline_no_line_with_line_mapper() { - let mapper = ProguardMapper::from(INLINE_NO_LINE_MAPPING); - - // With line 2, should match the inlined chain +fn test_inline_no_line_assume_no_inline_ambiguous_stacktrace() { let input = "\ Exception in thread \"main\" java.lang.NullPointerException - at a.foo(SourceFile:2) + at a.foo(Unknown Source) "; + // When no line info is available, prefer base (no-line) mappings if present. let expected = "\ Exception in thread \"main\" java.lang.NullPointerException - at retrace.Main.method1(Main.java:0) - at retrace.Main.main(Main.java:0) + at retrace.Main.otherMain(Main.java:0) "; + let mapper = ProguardMapper::from(INLINE_NO_LINE_ASSUME_NO_INLINE_AMBIGUOUS_MAPPING); let actual = mapper.remap_stacktrace(input).unwrap(); assert_eq!(actual.trim(), expected.trim()); -} -#[test] -fn test_inline_no_line_with_line_cache() { - let mapping = ProguardMapping::new(INLINE_NO_LINE_MAPPING.as_bytes()); + let mapping = + ProguardMapping::new(INLINE_NO_LINE_ASSUME_NO_INLINE_AMBIGUOUS_MAPPING.as_bytes()); let mut buf = Vec::new(); ProguardCache::write(&mapping, &mut buf).unwrap(); let cache = ProguardCache::parse(&buf).unwrap(); - - let input = "\ -Exception in thread \"main\" java.lang.NullPointerException - at a.foo(SourceFile:2) -"; - - let expected = "\ -Exception in thread \"main\" java.lang.NullPointerException - at retrace.Main.method1(Main.java:0) - at retrace.Main.main(Main.java:0) -"; + cache.test(); let actual = cache.remap_stacktrace(input).unwrap(); assert_eq!(actual.trim(), expected.trim()); } - -#[test] -fn test_inline_no_line_with_line_frame_mapper() { - let mapper = ProguardMapper::from(INLINE_NO_LINE_MAPPING); - - let frame = StackFrame::new("a", "foo", 2); - let frames: Vec<_> = mapper.remap_frame(&frame).collect(); - - assert_eq!(frames.len(), 2); - assert_eq!( - frames[0], - StackFrame::with_file("retrace.Main", "method1", 0, "Main.java") - ); - assert_eq!( - frames[1], - StackFrame::with_file("retrace.Main", "main", 0, "Main.java") - ); -} - -#[test] -fn test_inline_no_line_with_line_frame_cache() { - let mapping = ProguardMapping::new(INLINE_NO_LINE_MAPPING.as_bytes()); - let mut buf = Vec::new(); - ProguardCache::write(&mapping, &mut buf).unwrap(); - let cache = ProguardCache::parse(&buf).unwrap(); - - let frame = StackFrame::new("a", "foo", 2); - let frames: Vec<_> = cache.remap_frame(&frame).collect(); - - assert_eq!(frames.len(), 2); - assert_eq!( - frames[0], - StackFrame::with_file("retrace.Main", "method1", 0, "Main.java") - ); - assert_eq!( - frames[1], - StackFrame::with_file("retrace.Main", "main", 0, "Main.java") - ); -} From 1deef0ad25de1b871feb3c6be81bc930801d99d2 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 23 Dec 2025 13:17:51 +0100 Subject: [PATCH 03/20] Change expected of test_ambiguous_missing_line_stacktrace --- tests/r8-ambiguous.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/r8-ambiguous.rs b/tests/r8-ambiguous.rs index 579f848..e3b9bf7 100644 --- a/tests/r8-ambiguous.rs +++ b/tests/r8-ambiguous.rs @@ -78,14 +78,14 @@ Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3) let expected = "\ com.android.tools.r8.CompilationException: foo[parens](Source:3) - at com.android.tools.r8.R8.foo(R8.java:0) - at com.android.tools.r8.R8.bar(R8.java:0) - at com.android.tools.r8.R8.foo(R8.java:0) - at com.android.tools.r8.R8.bar(R8.java:0) + at com.android.tools.r8.R8.foo(R8.java:7) + at com.android.tools.r8.R8.bar(R8.java:7) + at com.android.tools.r8.R8.foo(R8.java:8) + at com.android.tools.r8.R8.bar(R8.java:8) at com.android.tools.r8.R8.main(Unknown Source) Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3) - at com.android.tools.r8.R8.foo(R8.java:0) - at com.android.tools.r8.R8.bar(R8.java:0) + at com.android.tools.r8.R8.foo(R8.java:9) + at com.android.tools.r8.R8.bar(R8.java:9) ... 42 more "; From 558ed430c0e87a6af7f3749f6829b1950270bf1d Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 23 Dec 2025 12:05:45 +0100 Subject: [PATCH 04/20] fix(r8-tests): Correctly handle no-line mappings --- src/cache/mod.rs | 208 +++++++++++++++++++++++++++++++++++++++++++--- src/cache/raw.rs | 5 -- src/mapper.rs | 88 ++++++++++++++++---- src/stacktrace.rs | 7 +- 4 files changed, 276 insertions(+), 32 deletions(-) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 1826829..f606e43 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -85,12 +85,13 @@ use crate::{java, stacktrace, DeobfuscatedSignature, StackFrame, StackTrace, Thr pub use raw::{ProguardCache, PRGCACHE_VERSION}; /// Result of looking up member mappings for a frame. -/// Contains: (members, prepared_frame, rewrite_rules, had_mappings, outer_source_file) +/// Contains: (members, prepared_frame, rewrite_rules, had_mappings, has_line_info, outer_source_file) type MemberLookupResult<'data> = ( &'data [raw::Member], StackFrame<'data>, Vec>, bool, + bool, Option<&'data str>, ); @@ -384,11 +385,14 @@ impl<'data> ProguardCache<'data> { } } + let has_line_info = mapping_entries.iter().any(|m| m.endline > 0); + Some(( mapping_entries, prepared_frame, rewrite_rules, had_mappings, + has_line_info, outer_source_file, )) } @@ -402,8 +406,14 @@ impl<'data> ProguardCache<'data> { &'r self, frame: &StackFrame<'data>, ) -> RemappedFrameIter<'r, 'data> { - let Some((members, prepared_frame, _rewrite_rules, _had_mappings, outer_source_file)) = - self.find_members_and_rules(frame) + let Some(( + members, + prepared_frame, + _rewrite_rules, + had_mappings, + has_line_info, + outer_source_file, + )) = self.find_members_and_rules(frame) else { return RemappedFrameIter::empty(); }; @@ -413,7 +423,8 @@ impl<'data> ProguardCache<'data> { prepared_frame, members.iter(), 0, - false, + had_mappings, + has_line_info, outer_source_file, ) } @@ -452,9 +463,32 @@ impl<'data> ProguardCache<'data> { let effective = self.prepare_frame_for_mapping(frame, carried_outline_pos); - let Some((members, prepared_frame, rewrite_rules, had_mappings, outer_source_file)) = - self.find_members_and_rules(&effective) + let Some(( + members, + prepared_frame, + rewrite_rules, + had_mappings, + has_line_info, + outer_source_file, + )) = self.find_members_and_rules(&effective) else { + // Even if we cannot resolve a member mapping, we may still be able to remap the class. + if let Some(class) = self.get_class(effective.class) { + let original_class = self + .read_string(class.original_name_offset) + .unwrap_or(effective.class); + let outer_source_file = self.read_string(class.file_name_offset).ok(); + let file = + synthesize_source_file(original_class, outer_source_file).map(Cow::Owned); + return Some(RemappedFrameIter::single(StackFrame { + class: original_class, + method: effective.method, + file, + line: effective.line, + parameters: effective.parameters, + method_synthesized: false, + })); + } return Some(RemappedFrameIter::empty()); }; @@ -471,6 +505,7 @@ impl<'data> ProguardCache<'data> { members.iter(), skip_count, had_mappings, + has_line_info, outer_source_file, )) } @@ -779,10 +814,14 @@ pub struct RemappedFrameIter<'r, 'data> { StackFrame<'data>, std::slice::Iter<'data, raw::Member>, )>, + /// A single remapped frame fallback (e.g. class-only remapping). + fallback: Option>, /// Number of frames to skip from rewrite rules. skip_count: usize, /// Whether there were mapping entries (for should_skip determination). had_mappings: bool, + /// Whether this method has any line-based mappings. + has_line_info: bool, /// The source file of the outer class for synthesis. outer_source_file: Option<&'data str>, } @@ -791,8 +830,10 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> { fn empty() -> Self { Self { inner: None, + fallback: None, skip_count: 0, had_mappings: false, + has_line_info: false, outer_source_file: None, } } @@ -803,16 +844,30 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> { members: std::slice::Iter<'data, raw::Member>, skip_count: usize, had_mappings: bool, + has_line_info: bool, outer_source_file: Option<&'data str>, ) -> Self { Self { inner: Some((cache, frame, members)), + fallback: None, skip_count, had_mappings, + has_line_info, outer_source_file, } } + fn single(frame: StackFrame<'data>) -> Self { + Self { + inner: None, + fallback: Some(frame), + skip_count: 0, + had_mappings: false, + has_line_info: false, + outer_source_file: None, + } + } + /// Returns whether there were mapping entries before rewrite rules were applied. /// /// After collecting frames, if `had_mappings()` is true but the result is empty, @@ -822,12 +877,106 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> { } fn next_inner(&mut self) -> Option> { - let (cache, frame, members) = self.inner.as_mut()?; - if frame.parameters.is_none() { - iterate_with_lines(cache, frame, members, self.outer_source_file) - } else { - iterate_without_lines(cache, frame, members, self.outer_source_file) + if let Some(frame) = self.fallback.take() { + return Some(frame); } + + let (cache, mut frame, mut members) = self.inner.take()?; + + let out = if frame.parameters.is_none() { + // If we have no line number, treat it as unknown. If there are base (no-line) mappings + // present, prefer those over line-mapped entries. + if frame.line == 0 { + let remaining = members.as_slice(); + // Prefer base entries (endline == 0) if present. + let mut base_members = remaining.iter().filter(|m| m.endline == 0); + if let Some(first_base) = base_members.next() { + // If all base entries resolve to the same original method name, deduplicate. + let all_same = base_members.all(|m| { + m.original_class_offset == first_base.original_class_offset + && m.original_name_offset == first_base.original_name_offset + }); + + if all_same { + let class = cache + .read_string(first_base.original_class_offset) + .unwrap_or(frame.class); + let method = cache.read_string(first_base.original_name_offset).ok()?; + let file = + synthesize_source_file(class, self.outer_source_file).map(Cow::Owned); + + return Some(StackFrame { + class, + method, + file, + line: 0, + parameters: frame.parameters, + method_synthesized: first_base.is_synthesized(), + }); + } + + // Multiple distinct base entries: iterate them (skip line-mapped entries). + let mapped = iterate_without_lines_preferring_base( + cache, + &mut frame, + &mut members, + self.outer_source_file, + ); + self.inner = Some((cache, frame, members)); + return mapped; + } + + // No base entries: fall back to existing behavior (may yield multiple candidates). + let first = remaining.first()?; + let unambiguous = remaining.iter().all(|m| { + m.original_class_offset == first.original_class_offset + && m.original_name_offset == first.original_name_offset + }); + + if unambiguous { + let class = cache + .read_string(first.original_class_offset) + .unwrap_or(frame.class); + let method = cache.read_string(first.original_name_offset).ok()?; + let file = + synthesize_source_file(class, self.outer_source_file).map(Cow::Owned); + + return Some(StackFrame { + class, + method, + file, + line: 0, + parameters: frame.parameters, + method_synthesized: first.is_synthesized(), + }); + } + + let mapped = + iterate_without_lines(cache, &mut frame, &mut members, self.outer_source_file); + self.inner = Some((cache, frame, members)); + return mapped; + } + + // With a concrete line number, skip base entries if there are line mappings. + let mapped = iterate_with_lines( + cache, + &mut frame, + &mut members, + self.outer_source_file, + self.has_line_info, + ); + self.inner = Some((cache, frame, members)); + mapped + } else { + let mapped = + iterate_without_lines(cache, &mut frame, &mut members, self.outer_source_file); + self.inner = Some((cache, frame, members)); + mapped + }; + + // If we returned early for the unambiguous line==0 case above, `self.inner` remains `None` + // which ensures the iterator terminates. + out } } @@ -849,8 +998,13 @@ fn iterate_with_lines<'a>( frame: &mut StackFrame<'a>, members: &mut std::slice::Iter<'_, raw::Member>, outer_source_file: Option<&str>, + has_line_info: bool, ) -> Option> { for member in members { + // If this method has line mappings, skip base (no-line) entries when we have a concrete line. + if has_line_info && frame.line > 0 && member.endline == 0 { + continue; + } // skip any members which do not match our frames line if member.endline > 0 && (frame.line < member.startline as usize || frame.line > member.endline as usize) @@ -902,6 +1056,38 @@ fn iterate_with_lines<'a>( None } +fn iterate_without_lines_preferring_base<'a>( + cache: &ProguardCache<'a>, + frame: &mut StackFrame<'a>, + members: &mut std::slice::Iter<'_, raw::Member>, + outer_source_file: Option<&str>, +) -> Option> { + for member in members { + if member.endline != 0 { + continue; + } + + let class = cache + .read_string(member.original_class_offset) + .unwrap_or(frame.class); + + let method = cache.read_string(member.original_name_offset).ok()?; + + // Synthesize from class name (input filename is not reliable) + let file = synthesize_source_file(class, outer_source_file).map(Cow::Owned); + + return Some(StackFrame { + class, + method, + file, + line: 0, + parameters: frame.parameters, + method_synthesized: member.is_synthesized(), + }); + } + None +} + fn iterate_without_lines<'a>( cache: &ProguardCache<'a>, frame: &mut StackFrame<'a>, diff --git a/src/cache/raw.rs b/src/cache/raw.rs index bd7f850..66a18a1 100644 --- a/src/cache/raw.rs +++ b/src/cache/raw.rs @@ -343,12 +343,7 @@ impl<'data> ProguardCache<'data> { .entry(obfuscated_method.as_str()) .or_default(); - let has_line_info = members.all.iter().any(|m| m.endline > 0); for member in members.all.iter() { - // Skip members without line information if there are members with line information - if has_line_info && member.startline == 0 && member.endline == 0 { - continue; - } method_mappings.push(Self::resolve_mapping( &mut string_table, &parsed, diff --git a/src/mapper.rs b/src/mapper.rs index ef3cb74..f1fa3df 100644 --- a/src/mapper.rs +++ b/src/mapper.rs @@ -102,15 +102,20 @@ type MemberIter<'m> = std::slice::Iter<'m, MemberMapping<'m>>; #[derive(Clone, Debug, Default)] pub struct RemappedFrameIter<'m> { inner: Option<(StackFrame<'m>, MemberIter<'m>)>, + has_line_info: bool, } impl<'m> RemappedFrameIter<'m> { fn empty() -> Self { - Self { inner: None } + Self { + inner: None, + has_line_info: false, + } } - fn members(frame: StackFrame<'m>, members: MemberIter<'m>) -> Self { + fn members(frame: StackFrame<'m>, members: MemberIter<'m>, has_line_info: bool) -> Self { Self { inner: Some((frame, members)), + has_line_info, } } } @@ -120,7 +125,7 @@ impl<'m> Iterator for RemappedFrameIter<'m> { fn next(&mut self) -> Option { let (frame, ref mut members) = self.inner.as_mut()?; if frame.parameters.is_none() { - iterate_with_lines(frame, members) + iterate_with_lines(frame, members, self.has_line_info) } else { iterate_without_lines(frame, members) } @@ -260,8 +265,13 @@ fn apply_rewrite_rules<'s>(collected: &mut CollectedFrames<'s>, thrown_descripto fn iterate_with_lines<'a>( frame: &mut StackFrame<'a>, members: &mut core::slice::Iter<'_, MemberMapping<'a>>, + has_line_info: bool, ) -> Option> { for member in members { + // If this method has line mappings, skip base (no-line) entries when we have a concrete line. + if has_line_info && frame.line > 0 && member.endline == 0 { + continue; + } if let Some(mapped) = map_member_with_lines(frame, member) { return Some(mapped); } @@ -361,12 +371,7 @@ impl<'s> ProguardMapper<'s> { .entry(obfuscated_method.as_str()) .or_default(); - let has_line_info = members.all.iter().any(|m| m.endline > 0); for member in members.all.iter() { - // Skip members without line information if there are members with line information - if has_line_info && member.startline == 0 && member.endline == 0 { - continue; - } method_mappings.all_mappings.push(Self::resolve_mapping( &parsed, member, @@ -519,13 +524,26 @@ impl<'s> ProguardMapper<'s> { let Some(class) = self.classes.get(frame.class) else { return collected; }; - let Some(members) = class.members.get(frame.method) else { - return collected; - }; let mut frame = frame.clone(); frame.class = class.original; + // If we don't have any member mappings, we can still remap the class name. + // This is especially important for stack frames where the method is not mapped or the + // stacktrace does not contain sufficient information to resolve the method. + let Some(members) = class.members.get(frame.method) else { + let file = synthesize_source_file(frame.class, frame.file()).map(Cow::Owned); + collected.frames.push(StackFrame { + class: frame.class, + method: frame.method, + file, + line: frame.line, + parameters: frame.parameters, + method_synthesized: false, + }); + return collected; + }; + let mapping_entries: &[MemberMapping<'s>] = if let Some(parameters) = frame.parameters { let Some(typed_members) = members.mappings_by_params.get(parameters) else { return collected; @@ -536,7 +554,48 @@ impl<'s> ProguardMapper<'s> { }; if frame.parameters.is_none() { + let has_line_info = mapping_entries.iter().any(|m| m.endline > 0); + + // If the stacktrace has no line number, treat it as unknown and remap without + // applying line filters. If there are base (no-line) mappings present, prefer those. + if frame.line == 0 { + let preferred: Vec<_> = if has_line_info { + let base: Vec<_> = mapping_entries.iter().filter(|m| m.endline == 0).collect(); + if base.is_empty() { + mapping_entries.iter().collect() + } else { + base + } + } else { + mapping_entries.iter().collect() + }; + + let mut members_iter = preferred.iter().copied(); + let Some(first) = members_iter.next() else { + return collected; + }; + + let unambiguous = members_iter.all(|m| m.original == first.original); + if unambiguous { + collected + .frames + .push(map_member_without_lines(&frame, first)); + collected.rewrite_rules.extend(first.rewrite_rules.iter()); + } else { + for member in preferred { + collected + .frames + .push(map_member_without_lines(&frame, member)); + collected.rewrite_rules.extend(member.rewrite_rules.iter()); + } + } + return collected; + } + for member in mapping_entries { + if has_line_info && member.endline == 0 { + continue; + } if let Some(mapped) = map_member_with_lines(&frame, member) { collected.frames.push(mapped); collected.rewrite_rules.extend(member.rewrite_rules.iter()); @@ -606,7 +665,8 @@ impl<'s> ProguardMapper<'s> { members.all_mappings.iter() }; - RemappedFrameIter::members(frame, mappings) + let has_line_info = members.all_mappings.iter().any(|m| m.endline > 0); + RemappedFrameIter::members(frame, mappings, has_line_info) } /// Remaps a throwable which is the first line of a full stacktrace. @@ -1016,7 +1076,7 @@ java.lang.IllegalStateException: Boom } #[test] - fn remap_frame_without_mapping_keeps_original_line() { + fn remap_frame_without_mapping_remaps_class_best_effort() { let mapping = "\ some.Class -> a: 1:1:void some.Class.existing():10:10 -> a @@ -1029,7 +1089,7 @@ java.lang.RuntimeException: boom "; let expected = "\ java.lang.RuntimeException: boom - at a.missing(SourceFile:42) + at some.Class.missing(Class.java:42) "; assert_eq!(mapper.remap_stacktrace(input).unwrap(), expected); diff --git a/src/stacktrace.rs b/src/stacktrace.rs index 09d0544..0eb427d 100644 --- a/src/stacktrace.rs +++ b/src/stacktrace.rs @@ -286,8 +286,10 @@ pub(crate) fn parse_frame(line: &str) -> Option> { let (method_split, file_split) = line[3..line.len() - 1].split_once('(')?; let (class, method) = method_split.rsplit_once('.')?; - let (file, line) = file_split.split_once(':')?; - let line = line.parse().ok()?; + let (file, line) = match file_split.split_once(':') { + Some((file, line)) => (file, line.parse().ok()?), + None => (file_split, 0), + }; Some(StackFrame { class, @@ -389,6 +391,7 @@ pub(crate) fn parse_throwable(line: &str) -> Option> { #[cfg(test)] mod tests { use super::*; + use std::borrow::Cow; #[test] fn print_stack_trace() { From 59a8dd7fbfc35807471934095d6827171573be0b Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 23 Dec 2025 12:08:01 +0100 Subject: [PATCH 05/20] Remove import --- src/stacktrace.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/stacktrace.rs b/src/stacktrace.rs index 0eb427d..875f4ca 100644 --- a/src/stacktrace.rs +++ b/src/stacktrace.rs @@ -391,7 +391,6 @@ pub(crate) fn parse_throwable(line: &str) -> Option> { #[cfg(test)] mod tests { use super::*; - use std::borrow::Cow; #[test] fn print_stack_trace() { From cf3f7328d22d6504d6397ed945d495bb2ec18d1f Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 23 Dec 2025 14:12:47 +0100 Subject: [PATCH 06/20] Preserve input lineno when no mapping is available --- src/cache/mod.rs | 32 ++++++++++++++++++++++++++++---- src/mapper.rs | 15 +++++++++++++-- tests/retrace.rs | 3 ++- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index f606e43..1884103 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -909,7 +909,8 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> { class, method, file, - line: 0, + // Preserve input line if present when the mapping has no line info. + line: frame.line, parameters: frame.parameters, method_synthesized: first_base.is_synthesized(), }); @@ -945,7 +946,8 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> { class, method, file, - line: 0, + // Preserve input line if present when the mapping has no line info. + line: frame.line, parameters: frame.parameters, method_synthesized: first.is_synthesized(), }); @@ -1005,6 +1007,26 @@ fn iterate_with_lines<'a>( if has_line_info && frame.line > 0 && member.endline == 0 { continue; } + // If the mapping entry has no line range, preserve the input line number (if any). + if member.endline == 0 { + let class = cache + .read_string(member.original_class_offset) + .unwrap_or(frame.class); + + let method = cache.read_string(member.original_name_offset).ok()?; + + // Synthesize from class name (input filename is not reliable) + let file = synthesize_source_file(class, outer_source_file).map(Cow::Owned); + + return Some(StackFrame { + class, + method, + file, + line: frame.line, + parameters: frame.parameters, + method_synthesized: member.is_synthesized(), + }); + } // skip any members which do not match our frames line if member.endline > 0 && (frame.line < member.startline as usize || frame.line > member.endline as usize) @@ -1080,7 +1102,8 @@ fn iterate_without_lines_preferring_base<'a>( class, method, file, - line: 0, + // Preserve input line if present when the mapping has no line info. + line: frame.line, parameters: frame.parameters, method_synthesized: member.is_synthesized(), }); @@ -1109,7 +1132,8 @@ fn iterate_without_lines<'a>( class, method, file, - line: 0, + // Preserve input line if present when the mapping has no line info. + line: frame.line, parameters: frame.parameters, method_synthesized: member.is_synthesized(), }) diff --git a/src/mapper.rs b/src/mapper.rs index f1fa3df..300102a 100644 --- a/src/mapper.rs +++ b/src/mapper.rs @@ -224,7 +224,9 @@ fn map_member_without_lines<'a>( class, method: member.original, file, - line: 0, + // Preserve input line if present (e.g. "Unknown Source:7") when the mapping itself + // has no line information. This matches R8 retrace behavior. + line: frame.line, parameters: frame.parameters, method_synthesized: member.is_synthesized, } @@ -272,6 +274,10 @@ fn iterate_with_lines<'a>( if has_line_info && frame.line > 0 && member.endline == 0 { continue; } + // If the mapping entry has no line range, preserve the input line number (if any). + if member.endline == 0 { + return Some(map_member_without_lines(frame, member)); + } if let Some(mapped) = map_member_with_lines(frame, member) { return Some(mapped); } @@ -596,7 +602,12 @@ impl<'s> ProguardMapper<'s> { if has_line_info && member.endline == 0 { continue; } - if let Some(mapped) = map_member_with_lines(&frame, member) { + if member.endline == 0 { + collected + .frames + .push(map_member_without_lines(&frame, member)); + collected.rewrite_rules.extend(member.rewrite_rules.iter()); + } else if let Some(mapped) = map_member_with_lines(&frame, member) { collected.frames.push(mapped); collected.rewrite_rules.extend(member.rewrite_rules.iter()); } diff --git a/tests/retrace.rs b/tests/retrace.rs index ca9fc5e..ca54fae 100644 --- a/tests/retrace.rs +++ b/tests/retrace.rs @@ -71,7 +71,8 @@ fn test_remap_no_lines() { let mut mapped = mapper.remap_frame(&StackFrame::new("a", "b", 10)); assert_eq!( mapped.next().unwrap(), - StackFrame::with_file("original.class.name", "originalMethodName", 0, "name.java") + // Preserve input line number when the mapping has no line information. + StackFrame::with_file("original.class.name", "originalMethodName", 10, "name.java") ); assert_eq!(mapped.next(), None); } From 147a43eb546c47e18ed0b8dc13cc19b6fd8fc419 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 23 Dec 2025 13:03:24 +0100 Subject: [PATCH 07/20] feat(r8-tests): Add R8 synthetic tests --- tests/r8-synthetic.rs | 152 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 tests/r8-synthetic.rs diff --git a/tests/r8-synthetic.rs b/tests/r8-synthetic.rs new file mode 100644 index 0000000..97d0b68 --- /dev/null +++ b/tests/r8-synthetic.rs @@ -0,0 +1,152 @@ +//! Tests for R8 synthetic / lambda method retracing fixtures. +//! +//! These tests are based on the R8 retrace test suite from: +//! src/test/java/com/android/tools/r8/retrace/stacktraces/ + +use proguard::{ProguardCache, ProguardMapper, ProguardMapping}; + +// ============================================================================= +// SyntheticLambdaMethodStackTrace +// ============================================================================= + +const SYNTHETIC_LAMBDA_METHOD_MAPPING: &str = "\ +# {'id':'com.android.tools.r8.mapping','version':'1.0'} +example.Main -> example.Main: + 1:1:void main(java.lang.String[]):123 -> main +example.Foo -> a.a: + 5:5:void lambda$main$0():225 -> a + 3:3:void runIt():218 -> b + 2:2:void main():223 -> c +example.Foo$$ExternalSyntheticLambda0 -> a.b: + void run(example.Foo) -> a + # {'id':'com.android.tools.r8.synthesized'} +"; + +#[test] +fn test_synthetic_lambda_method_stacktrace() { + let input = "\ +Exception in thread \"main\" java.lang.NullPointerException + at a.a.a(a.java:5) + at a.b.a(Unknown Source) + at a.a.b(a.java:3) + at a.a.c(a.java:2) + at example.Main.main(Main.java:1) +"; + + // Note: this crate prints 4 spaces before each remapped frame. + // Also, when no line info is available, it will output :0. + let expected = "\ +Exception in thread \"main\" java.lang.NullPointerException + at example.Foo.lambda$main$0(Foo.java:225) + at example.Foo.runIt(Foo.java:218) + at example.Foo.main(Foo.java:223) + at example.Main.main(Main.java:123) +"; + + let mapper = ProguardMapper::from(SYNTHETIC_LAMBDA_METHOD_MAPPING); + let actual = mapper.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); + + let mapping = ProguardMapping::new(SYNTHETIC_LAMBDA_METHOD_MAPPING.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + cache.test(); + + let actual = cache.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} + +// ============================================================================= +// SyntheticLambdaMethodWithInliningStackTrace +// ============================================================================= + +const SYNTHETIC_LAMBDA_METHOD_WITH_INLINING_MAPPING: &str = "\ +# {'id':'com.android.tools.r8.mapping','version':'1.0'} +example.Main -> example.Main: + 1:1:void main(java.lang.String[]):123 -> main +example.Foo -> a.a: + 3:3:void runIt():218 -> b + 2:2:void main():223 -> c +example.Foo$$ExternalSyntheticLambda0 -> a.b: + 4:4:void example.Foo.lambda$main$0():225 -> a + 4:4:void run(example.Foo):0 -> a + # {'id':'com.android.tools.r8.synthesized'} +"; + +#[test] +fn test_synthetic_lambda_method_with_inlining_stacktrace() { + let input = "\ +Exception in thread \"main\" java.lang.NullPointerException + at a.b.a(Unknown Source:4) + at a.a.b(a.java:3) + at a.a.c(a.java:2) + at example.Main.main(Main.java:1) +"; + + let expected = "\ +Exception in thread \"main\" java.lang.NullPointerException + at example.Foo.lambda$main$0(Foo.java:225) + at example.Foo.runIt(Foo.java:218) + at example.Foo.main(Foo.java:223) + at example.Main.main(Main.java:123) +"; + + let mapper = ProguardMapper::from(SYNTHETIC_LAMBDA_METHOD_WITH_INLINING_MAPPING); + let actual = mapper.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); + + let mapping = ProguardMapping::new(SYNTHETIC_LAMBDA_METHOD_WITH_INLINING_MAPPING.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + cache.test(); + + let actual = cache.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} + +// ============================================================================= +// MovedSynthetizedInfoStackTraceTest +// ============================================================================= + +const MOVED_SYNTHETIZED_INFO_MAPPING: &str = "\ +# { id: 'com.android.tools.r8.mapping', version: '2.2' } +com.android.tools.r8.BaseCommand$Builder -> foo.bar: + 1:1:void inlinee(java.util.Collection):0:0 -> inlinee$synthetic + 1:1:void inlinee$synthetic(java.util.Collection):0:0 -> inlinee$synthetic + 2:2:void inlinee(java.util.Collection):206:206 -> inlinee$synthetic + 2:2:void inlinee$synthetic(java.util.Collection):0:0 -> inlinee$synthetic + # {\"id\":\"com.android.tools.r8.synthesized\"} + 4:4:void inlinee(java.util.Collection):208:208 -> inlinee$synthetic + 4:4:void inlinee$synthetic(java.util.Collection):0 -> inlinee$synthetic + 7:7:void error(origin.Origin,java.lang.Throwable):363:363 -> inlinee$synthetic + 7:7:void inlinee(java.util.Collection):210 -> inlinee$synthetic + 7:7:void inlinee$synthetic(java.util.Collection):0:0 -> inlinee$synthetic +"; + +#[test] +fn test_moved_synthetized_info_stacktrace() { + let input = "\ +java.lang.RuntimeException: foobar + at foo.bar.inlinee$synthetic(BaseCommand.java:2) +"; + + let expected = "\ +java.lang.RuntimeException: foobar + at com.android.tools.r8.BaseCommand$Builder.inlinee(BaseCommand.java:206) +"; + + let mapper = ProguardMapper::from(MOVED_SYNTHETIZED_INFO_MAPPING); + let actual = mapper.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); + + let mapping = ProguardMapping::new(MOVED_SYNTHETIZED_INFO_MAPPING.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + cache.test(); + + let actual = cache.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} From 7800a1f48b36bed75d5ad82901bd3dcdbbefba02 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 23 Dec 2025 16:58:54 +0100 Subject: [PATCH 08/20] feat(r8-tests): Add R8 source file tests --- tests/r8-source-file-edge-cases.rs | 224 +++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 tests/r8-source-file-edge-cases.rs diff --git a/tests/r8-source-file-edge-cases.rs b/tests/r8-source-file-edge-cases.rs new file mode 100644 index 0000000..e428cbb --- /dev/null +++ b/tests/r8-source-file-edge-cases.rs @@ -0,0 +1,224 @@ +//! Tests for R8 retrace "Source File Edge Cases" fixtures. +//! +//! These tests are ported from the upstream R8 retrace fixtures in: +//! `src/test/java/com/android/tools/r8/retrace/stacktraces/`. +#![allow(clippy::unwrap_used)] + +use proguard::{ProguardCache, ProguardMapper, ProguardMapping}; + +fn assert_remap_stacktrace(mapping: &str, input: &str, expected: &str) { + let mapper = ProguardMapper::from(mapping); + let actual = mapper.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim_end(), expected.trim_end()); + + let mapping = ProguardMapping::new(mapping.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + cache.test(); + + let actual = cache.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim_end(), expected.trim_end()); +} + +// ============================================================================= +// ColonInFileNameStackTrace +// ============================================================================= + +const COLON_IN_FILE_NAME_MAPPING: &str = "\ +some.Class -> a: +# {\"id\":\"sourceFile\",\"fileName\":\"Class.kt\"} + 1:3:int strawberry(int):99:101 -> s + 4:5:int mango(float):121:122 -> s + int passionFruit(float):121:121 -> t +"; + +#[test] +fn test_colon_in_file_name_stacktrace() { + // Preserve leading whitespace exactly (no accidental leading newline). + let input = r#" at a.s(:foo::bar:1) + at a.t(:foo::bar:) +"#; + + let expected = r#" at some.Class.strawberry(Class.kt:99) + at some.Class.passionFruit(Class.kt:121) +"#; + + assert_remap_stacktrace(COLON_IN_FILE_NAME_MAPPING, input, expected); +} + +// ============================================================================= +// UnicodeInFileNameStackTrace +// ============================================================================= + +const UNICODE_IN_FILE_NAME_MAPPING: &str = "\ +some.Class -> a: +# {\"id\":\"sourceFile\",\"fileName\":\"Class.kt\"} + 1:3:int strawberry(int):99:101 -> s + 4:5:int mango(float):121:122 -> s +"; + +#[test] +fn test_unicode_in_file_name_stacktrace() { + let input = r#" at a.s(Blåbærgrød.jàvà:1) +"#; + + // Normalize indentation to this crate's output (`" at ..."`) + let expected = r#" at some.Class.strawberry(Class.kt:99) +"#; + + assert_remap_stacktrace(UNICODE_IN_FILE_NAME_MAPPING, input, expected); +} + +// ============================================================================= +// MultipleDotsInFileNameStackTrace +// ============================================================================= + +const MULTIPLE_DOTS_IN_FILE_NAME_MAPPING: &str = "\ +some.Class -> a: +# {\"id\":\"sourceFile\",\"fileName\":\"Class.kt\"} + 1:3:int strawberry(int):99:101 -> s + 4:5:int mango(float):121:122 -> s +"; + +#[test] +fn test_multiple_dots_in_file_name_stacktrace() { + let input = r#" at a.s(foo.bar.baz:1) +"#; + + // Normalize indentation to this crate's output (`" at ..."`) + let expected = r#" at some.Class.strawberry(Class.kt:99) +"#; + + assert_remap_stacktrace(MULTIPLE_DOTS_IN_FILE_NAME_MAPPING, input, expected); +} + +// ============================================================================= +// FileNameExtensionStackTrace +// ============================================================================= + +const FILE_NAME_EXTENSION_MAPPING: &str = "\ +foo.bar.baz -> a.b.c: +R8 -> R8: +"; + +#[test] +fn test_file_name_extension_stacktrace() { + // Preserve upstream whitespace exactly (no accidental leading newline). + let input = r#"a.b.c: Problem when compiling program + at R8.main(App:800) + at R8.main(Native Method) + at R8.main(Main.java:) + at R8.main(Main.kt:1) + at R8.main(Main.foo) + at R8.main() + at R8.main(Unknown) + at R8.main(SourceFile) + at R8.main(SourceFile:1) +Suppressed: a.b.c: You have to write the program first + at R8.retrace(App:184) + ... 7 more +"#; + + let expected = r#"foo.bar.baz: Problem when compiling program + at R8.main(R8.java:800) + at R8.main(Native Method) + at R8.main(R8.java) + at R8.main(R8.kt:1) + at R8.main(R8.java) + at R8.main(R8.java) + at R8.main(R8.java) + at R8.main(R8.java) + at R8.main(R8.java:1) +Suppressed: foo.bar.baz: You have to write the program first + at R8.retrace(R8.java:184) + ... 7 more +"#; + + assert_remap_stacktrace(FILE_NAME_EXTENSION_MAPPING, input, expected); +} + +// ============================================================================= +// SourceFileNameSynthesizeStackTrace +// ============================================================================= + +const SOURCE_FILE_NAME_SYNTHESIZE_MAPPING: &str = "\ +android.support.v7.widget.ActionMenuView -> mapping: + 21:21:void invokeItem():624 -> a +android.support.v7.widget.ActionMenuViewKt -> mappingKotlin: + 21:21:void invokeItem():624 -> b +"; + +#[test] +fn test_source_file_name_synthesize_stacktrace() { + // Preserve upstream whitespace exactly (no accidental leading newline). + let input = r#" at mapping.a(AW779999992:21) + at noMappingKt.noMapping(AW779999992:21) + at mappingKotlin.b(AW779999992:21) +"#; + + // Normalize indentation for remapped frames to match this crate (`" at ..."`). The middle + // line is intentionally unmapped and keeps its original leading tab. + let expected = r#" at android.support.v7.widget.ActionMenuView.invokeItem(ActionMenuView.java:624) + at noMappingKt.noMapping(AW779999992:21) + at android.support.v7.widget.ActionMenuViewKt.invokeItem(ActionMenuView.kt:624) +"#; + + assert_remap_stacktrace(SOURCE_FILE_NAME_SYNTHESIZE_MAPPING, input, expected); +} + +// ============================================================================= +// SourceFileWithNumberAndEmptyStackTrace +// ============================================================================= + +const SOURCE_FILE_WITH_NUMBER_AND_EMPTY_MAPPING: &str = "\ +com.android.tools.r8.R8 -> com.android.tools.r8.R8: + 34:34:void com.android.tools.r8.utils.ExceptionUtils.withR8CompilationHandler(com.android.tools.r8.utils.Reporter,com.android.tools.r8.utils.ExceptionUtils$CompileAction):59:59 -> a + 34:34:void runForTesting(com.android.tools.r8.utils.AndroidApp,com.android.tools.r8.utils.InternalOptions):261 -> a +"; + +#[test] +fn test_source_file_with_number_and_empty_stacktrace() { + // Preserve upstream whitespace exactly (no accidental leading newline). + let input = r#" at com.android.tools.r8.R8.a(R.java:34) + at com.android.tools.r8.R8.a(:34) +"#; + + // Normalize indentation to this crate's output (`" at ..."`) + let expected = r#" at com.android.tools.r8.utils.ExceptionUtils.withR8CompilationHandler(ExceptionUtils.java:59) + at com.android.tools.r8.R8.runForTesting(R8.java:261) + at com.android.tools.r8.utils.ExceptionUtils.withR8CompilationHandler(ExceptionUtils.java:59) + at com.android.tools.r8.R8.runForTesting(R8.java:261) +"#; + + assert_remap_stacktrace(SOURCE_FILE_WITH_NUMBER_AND_EMPTY_MAPPING, input, expected); +} + +// ============================================================================= +// ClassWithDashStackTrace +// ============================================================================= + +const CLASS_WITH_DASH_MAPPING: &str = "\ +# {\"id\":\"com.android.tools.r8.mapping\",\"version\":\"1.0\"} +Unused -> I$-CC: +# {\"id\":\"com.android.tools.r8.synthesized\"} + 66:66:void I.staticMethod() -> staticMethod + 66:66:void staticMethod():0 -> staticMethod + # {\"id\":\"com.android.tools.r8.synthesized\"} +"; + +#[test] +fn test_class_with_dash_stacktrace() { + // Preserve upstream whitespace exactly (no accidental leading newline). + let input = r#"java.lang.NullPointerException + at I$-CC.staticMethod(I.java:66) + at Main.main(Main.java:73) +"#; + + let expected = r#"java.lang.NullPointerException + at I.staticMethod(I.java:66) + at Main.main(Main.java:73) +"#; + + assert_remap_stacktrace(CLASS_WITH_DASH_MAPPING, input, expected); +} From 6989e38f55b49626432278e43e6e00e59f21ccc1 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 8 Jan 2026 14:13:02 +0100 Subject: [PATCH 09/20] Fix fixtures --- tests/r8-source-file-edge-cases.rs | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/tests/r8-source-file-edge-cases.rs b/tests/r8-source-file-edge-cases.rs index e428cbb..7b94c0a 100644 --- a/tests/r8-source-file-edge-cases.rs +++ b/tests/r8-source-file-edge-cases.rs @@ -35,7 +35,6 @@ some.Class -> a: #[test] fn test_colon_in_file_name_stacktrace() { - // Preserve leading whitespace exactly (no accidental leading newline). let input = r#" at a.s(:foo::bar:1) at a.t(:foo::bar:) "#; @@ -86,7 +85,6 @@ fn test_multiple_dots_in_file_name_stacktrace() { let input = r#" at a.s(foo.bar.baz:1) "#; - // Normalize indentation to this crate's output (`" at ..."`) let expected = r#" at some.Class.strawberry(Class.kt:99) "#; @@ -104,7 +102,6 @@ R8 -> R8: #[test] fn test_file_name_extension_stacktrace() { - // Preserve upstream whitespace exactly (no accidental leading newline). let input = r#"a.b.c: Problem when compiling program at R8.main(App:800) at R8.main(Native Method) @@ -122,13 +119,13 @@ Suppressed: a.b.c: You have to write the program first let expected = r#"foo.bar.baz: Problem when compiling program at R8.main(R8.java:800) - at R8.main(Native Method) - at R8.main(R8.java) + at R8.main(R8.java:0) + at R8.main(R8.java:0) at R8.main(R8.kt:1) - at R8.main(R8.java) - at R8.main(R8.java) - at R8.main(R8.java) - at R8.main(R8.java) + at R8.main(R8.java:0) + at R8.main(R8.java:0) + at R8.main(R8.java:0) + at R8.main(R8.java:0) at R8.main(R8.java:1) Suppressed: foo.bar.baz: You have to write the program first at R8.retrace(R8.java:184) @@ -144,21 +141,18 @@ Suppressed: foo.bar.baz: You have to write the program first const SOURCE_FILE_NAME_SYNTHESIZE_MAPPING: &str = "\ android.support.v7.widget.ActionMenuView -> mapping: - 21:21:void invokeItem():624 -> a + 21:21:void invokeItem():624 -> a android.support.v7.widget.ActionMenuViewKt -> mappingKotlin: - 21:21:void invokeItem():624 -> b + 21:21:void invokeItem():624 -> b "; #[test] fn test_source_file_name_synthesize_stacktrace() { - // Preserve upstream whitespace exactly (no accidental leading newline). let input = r#" at mapping.a(AW779999992:21) at noMappingKt.noMapping(AW779999992:21) at mappingKotlin.b(AW779999992:21) "#; - // Normalize indentation for remapped frames to match this crate (`" at ..."`). The middle - // line is intentionally unmapped and keeps its original leading tab. let expected = r#" at android.support.v7.widget.ActionMenuView.invokeItem(ActionMenuView.java:624) at noMappingKt.noMapping(AW779999992:21) at android.support.v7.widget.ActionMenuViewKt.invokeItem(ActionMenuView.kt:624) @@ -173,18 +167,16 @@ fn test_source_file_name_synthesize_stacktrace() { const SOURCE_FILE_WITH_NUMBER_AND_EMPTY_MAPPING: &str = "\ com.android.tools.r8.R8 -> com.android.tools.r8.R8: - 34:34:void com.android.tools.r8.utils.ExceptionUtils.withR8CompilationHandler(com.android.tools.r8.utils.Reporter,com.android.tools.r8.utils.ExceptionUtils$CompileAction):59:59 -> a - 34:34:void runForTesting(com.android.tools.r8.utils.AndroidApp,com.android.tools.r8.utils.InternalOptions):261 -> a + 34:34:void com.android.tools.r8.utils.ExceptionUtils.withR8CompilationHandler(com.android.tools.r8.utils.Reporter,com.android.tools.r8.utils.ExceptionUtils$CompileAction):59:59 -> a + 34:34:void runForTesting(com.android.tools.r8.utils.AndroidApp,com.android.tools.r8.utils.InternalOptions):261 -> a "; #[test] fn test_source_file_with_number_and_empty_stacktrace() { - // Preserve upstream whitespace exactly (no accidental leading newline). let input = r#" at com.android.tools.r8.R8.a(R.java:34) at com.android.tools.r8.R8.a(:34) "#; - // Normalize indentation to this crate's output (`" at ..."`) let expected = r#" at com.android.tools.r8.utils.ExceptionUtils.withR8CompilationHandler(ExceptionUtils.java:59) at com.android.tools.r8.R8.runForTesting(R8.java:261) at com.android.tools.r8.utils.ExceptionUtils.withR8CompilationHandler(ExceptionUtils.java:59) @@ -209,7 +201,6 @@ Unused -> I$-CC: #[test] fn test_class_with_dash_stacktrace() { - // Preserve upstream whitespace exactly (no accidental leading newline). let input = r#"java.lang.NullPointerException at I$-CC.staticMethod(I.java:66) at Main.main(Main.java:73) From e40221fbceba6a0ad35f8dbfb746f0e2939a7a30 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 8 Jan 2026 20:51:22 +0100 Subject: [PATCH 10/20] Add MD file explaining failures --- tests/r8-source-file-edge-cases.NOTES.md | 60 ++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/r8-source-file-edge-cases.NOTES.md diff --git a/tests/r8-source-file-edge-cases.NOTES.md b/tests/r8-source-file-edge-cases.NOTES.md new file mode 100644 index 0000000..73a73f6 --- /dev/null +++ b/tests/r8-source-file-edge-cases.NOTES.md @@ -0,0 +1,60 @@ +# R8 Retrace: Source File Edge Cases — Remaining Fixes + +After normalizing fixture indentation (member mappings use 4 spaces), `tests/r8-source-file-edge-cases.rs` has **4 passing** and **3 failing** tests. + +This note documents, **one-by-one**, what still needs fixing in the crate (not in the fixtures) to make the remaining tests pass. + +## 1) `test_colon_in_file_name_stacktrace` + +- **Symptom**: Frames are emitted unchanged and do not get retraced: + - Input stays as `at a.s(:foo::bar:1)` / `at a.t(:foo::bar:)`. +- **Root cause**: `src/stacktrace.rs::parse_frame` splits `(:)` using the **first** `:`. + - For `(:foo::bar:1)`, the first split produces `file=""` and `line="foo::bar:1"`, so parsing the line number fails and the whole frame is rejected. +- **Fix needed**: + - In `parse_frame`, split `file:line` using the **last** colon (`rsplit_once(':')`) so file names can contain `:` (Windows paths and this fixture). + - Treat an empty or non-numeric suffix after the last colon as “no line info” (line `0`) instead of rejecting the frame. + +## 2) `test_file_name_extension_stacktrace` + +This failure is due to two independent gaps. + +### 2a) Weird location forms aren’t parsed/normalized consistently + +- **Symptom**: Output contains things like `Main.java:` and `R8.foo:0` instead of normalized `R8.java:0` for “no line” cases. +- **Root cause**: `parse_frame` only supports: + - `(:)`, or + - `()` (treated as line `0`), + and it currently rejects or mis-interprets inputs like: + - `(Native Method)`, `(Unknown Source)`, `(Unknown)`, `()` + - `(Main.java:)` (empty “line” part) + - `(Main.foo)` (no `:line`, but also not a normal source file extension) +- **Fix needed**: + - Make `parse_frame` permissive for these Java stacktrace forms and interpret them as a parsed frame with **line `0`** so remapping can then replace the file with the mapping’s source file (here: `R8.java`). + - Also apply the “split on last colon” rule from (1) so `file:line` parsing is robust. + +### 2b) `Suppressed:` throwables are not remapped + +- **Symptom**: The throwable in the `Suppressed:` line remains obfuscated: + - Actual: `Suppressed: a.b.c: You have to write the program first` + - Expected: `Suppressed: foo.bar.baz: You have to write the program first` +- **Root cause**: `src/mapper.rs::remap_stacktrace` remaps: + - the first-line throwable, and + - `Caused by: ...`, + but it does **not** detect/handle `Suppressed: ...`. +- **Fix needed**: + - Add handling for the `Suppressed: ` prefix analogous to `Caused by: `: + - parse the throwable after the prefix, + - remap it, + - emit with the same prefix. + +## 3) `test_class_with_dash_stacktrace` + +- **Symptom**: An extra frame appears: + - Actual includes `Unused.staticMethod(Unused.java:0)` in addition to `I.staticMethod(I.java:66)`. +- **Root cause**: The mapping includes synthesized metadata (`com.android.tools.r8.synthesized`) and multiple plausible remapped frames, including synthesized “holder/bridge” frames. + - Today we emit all candidates rather than preferring non-synthesized frames. +- **Fix needed**: + - Propagate the synthesized marker into `StackFrame.method_synthesized` during mapping. + - When multiple candidate remapped frames exist for one obfuscated frame, **filter synthesized frames** if any non-synthesized frames exist (or apply an equivalent preference rule). + + From 20a5bee03b5f63a092a3770a5b340994b6e2782c Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 19 Jan 2026 14:10:29 +0100 Subject: [PATCH 11/20] feat(r8-tests): Add R8 line number handling tests --- tests/r8-line-number-handling.NOTES.md | 122 ++++++++++ tests/r8-line-number-handling.rs | 313 +++++++++++++++++++++++++ 2 files changed, 435 insertions(+) create mode 100644 tests/r8-line-number-handling.NOTES.md create mode 100644 tests/r8-line-number-handling.rs diff --git a/tests/r8-line-number-handling.NOTES.md b/tests/r8-line-number-handling.NOTES.md new file mode 100644 index 0000000..a475f7e --- /dev/null +++ b/tests/r8-line-number-handling.NOTES.md @@ -0,0 +1,122 @@ +# R8 Retrace: Line Number Handling — Current Failures & Needed Fixes + +This note accompanies `tests/r8-line-number-handling.rs`. + +Status when this note was written: + +- **10 tests total** +- **2 passing**: `test_obfuscated_range_to_single_line_stacktrace`, `test_preamble_line_number_stacktrace` +- **8 failing**: listed below + +Like other ported suites, these tests: + +- **Omit upstream `` markers** and list alternatives as duplicate frames. +- **Normalize expected indentation** to this crate’s output (`" at ..."`). +- **Use `:0`** for “no line info” since this crate represents missing line numbers as `0`. + +Below is a **one-by-one** explanation of the remaining failures and what behavior in the crate likely needs fixing. + +## 1) `test_no_obfuscation_range_mapping_with_stacktrace` + +- **Expected**: + - `foo.a(…:0)` retraces to `foo(long):1:1` → `Main.foo(Main.java:1)` + - `foo.b(…:2)` retraces to `bar(int):3` → `Main.bar(Main.java:3)` + - For `0:0` and `0` mappings, upstream expects “use original line info semantics” (see upstream fixture comment). +- **Actual**: + - `Main.foo(Main.java:0)` (lost the `:1`) + - `Main.bar(Main.java:2)` (seems to preserve the **minified** line `2` rather than mapping to `3`) + - `baz` and `main` keep minified lines `8`/`7` rather than dropping/normalizing. +- **What needs fixing**: + - The crate’s “base mapping” (`0`, `0:0`) line-number semantics don’t match R8: + - Some cases should map to the **original** line (e.g. `:1` for `foo`) + - Some cases should prefer the **method’s declared original line** even when minified line is present (e.g. `bar(int):3`) + - Some `0:0` entries should use the **stacktrace line** (R8’s special-case behavior). + - The logic likely lives in member selection / line translation in `src/mapper.rs` / cache iteration paths. + +## 2) `test_multiple_lines_no_line_number_stacktrace` + +- **Expected** (no line in stacktrace): + - Choose the `0:0` entries (base mappings) and emit their original lines: + - `method1(Main.java:42)` and `main(Main.java:28)` +- **Actual**: + - Emits both with `:0`. +- **What needs fixing**: + - When the stacktrace has “no line” (`Unknown Source`), and the mapping provides `0:0:…:origLine:origLine` (or explicit original line metadata), we should be able to emit those original lines instead of forcing `0`. + - Today we are collapsing “unknown line” to numeric `0` too early and then losing the mapping’s original line information. + +## 3) `test_single_line_no_line_number_stacktrace` + +- **Expected**: + - Base mappings (`0:0`) for `a` and `b` should expand into multiple original methods (`method1` + `main`, etc.) with specific original lines where available. + - For `c`, upstream emits two alternatives (`main3` and `method3`) and preserves their source context. + - `main4` should preserve its declared original line `153`. +- **Actual**: + - Everything ends up as `:0` (e.g. `method1(Main.java:0)`, `main4(Main.java:0)`). +- **What needs fixing**: + - Same core issue as (3), but more visible: + - Preserve/emit mapping-derived original lines for `0:0` entries. + - Don’t convert “unknown” into `0` in a way that prevents later line reconstruction. + +## 4) `test_no_obfuscated_line_number_with_override` + +- **Expected**: + - `main(Unknown Source)` still maps to `Main.main(Main.java:3)` because the mapping has a single `main(...):3`. + - `overload(Unknown Source)` yields both overloads but without line suffixes in the non-verbose output. + - `mainPC(:3)` should map to `Main.java:42` (mapping line is `42`). +- **Actual**: + - Most frames show `:0`, and `mainPC` shows `:3` (minified line preserved) instead of `:42`. +- **What needs fixing**: + - When obfuscated line numbers are missing (`Unknown Source`) but mapping provides a concrete original line, we should emit it (e.g. `main:3`). + - For `mainPC(:3)`, we’re not translating minified `3` to original `42` even though the mapping is unambiguous. + - This points to incorrect or missing “no obfuscated line number override” behavior in remapping. + +## 5) `test_different_line_number_span_stacktrace` + +- **Expected**: + - The mapping says `method1(...):42:44 -> a` and the stacktrace is `a.a(…:1)`. + - Upstream expands this to **three** possible original lines `42`, `43`, `44` (span). +- **Actual**: + - Only one frame, and it uses the minified line `1` as the output line. +- **What needs fixing**: + - For mappings that define a span of original lines for a single minified line (or ambiguous mapping within a span), we need to expand into the full set of candidate original lines rather than carrying through the minified line. + - This is core “line span expansion” logic (member lookup + line translation). + +## 6) `test_outside_line_range_stacktrace` + +- **Expected**: + - `a.a(:2)` and `a.a(Unknown Source)` both map to `some.other.Class.method1(Class.java:42)` + - `b.a(:27)` maps to `some.Class.a(Class.java:27)` (outside any range → fall back to the “unmapped member name” for that class, per fixture) + - `b.a(Unknown Source)` maps to `some.Class.method2(Class.java)` (no line) +- **Actual**: + - `some.other.Class.method1(Class.java:2)` and `...:0` (line propagation wrong) + - One line remains unparsed and unchanged: `at b.a(:27)` (it is emitted verbatim when not remapped) + - Last frame becomes `method2(Class.java:0)` instead of `method2(Class.java)` +- **What needs fixing**: + - **Parsing / fallback**: the `(:27)` location should be parsed and then remapped (or at least “best-effort” remapped), but currently it falls back to printing the original frame line. + - **Outside-range semantics**: when the minified line is outside any mapped range, decide how to choose: + - either fall back to a “best effort” member name remap, + - or keep obfuscated, but the expected behavior is a best-effort remap. + - **No-line formatting**: `Class.java` vs `Class.java:0` (same as (1)). + +## 7) `test_invalid_minified_range_stacktrace` + +- **Expected**: + - Even though the mapping has an invalid minified range (`5:3`), upstream still retraces the method and produces `Main.java:3`. +- **Actual**: + - The input line is emitted unchanged (not retraced). +- **What needs fixing**: + - The mapping parser / remapper currently rejects or ignores invalid minified ranges entirely. + - Upstream seems to treat this as recoverable and still uses the information to retrace. + - Implement more tolerant handling of invalid minified ranges (or normalize them) so retrace still occurs. + +## 8) `test_invalid_original_range_stacktrace` + +- **Expected**: + - For an invalid original range (`:5:2`), upstream still retraces and emits `Main.java:3`. +- **Actual**: + - Emits `Main.java:6` (wrong translation). +- **What needs fixing**: + - The translation logic from minified line → original line is not handling inverted original ranges correctly. + - Needs clamping / normalization rules consistent with R8 (e.g. treat as single line, or swap, or ignore original span and use minified). + + diff --git a/tests/r8-line-number-handling.rs b/tests/r8-line-number-handling.rs new file mode 100644 index 0000000..3b0de55 --- /dev/null +++ b/tests/r8-line-number-handling.rs @@ -0,0 +1,313 @@ +//! Tests for R8 retrace "Line Number Handling" fixtures. +//! +//! Ported from the upstream R8 retrace fixtures in: +//! `src/test/java/com/android/tools/r8/retrace/stacktraces/`. +//! +//! Notes: +//! - These tests intentionally **omit** upstream `` markers and instead list alternative frames +//! as duplicates, since this crate does not currently format `` groups. +//! - Fixture mapping indentation is normalized to 4-space member indentation so it is parsed by this +//! crate's Proguard mapping parser. +//! - Expected stacktrace indentation is normalized to this crate's output (`" at ..."`). +#![allow(clippy::unwrap_used)] + +use proguard::{ProguardCache, ProguardMapper, ProguardMapping}; + +fn assert_remap_stacktrace(mapping: &str, input: &str, expected: &str) { + let mapper = ProguardMapper::from(mapping); + let actual = mapper.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim_end(), expected.trim_end()); + + let mapping = ProguardMapping::new(mapping.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + cache.test(); + + let actual = cache.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim_end(), expected.trim_end()); +} + +// ============================================================================= +// NoObfuscationRangeMappingWithStackTrace +// ============================================================================= + +const NO_OBFUSCATION_RANGE_MAPPING_WITH_STACKTRACE_MAPPING: &str = r#"com.android.tools.r8.naming.retrace.Main -> foo: + void foo(long):1:1 -> a + void bar(int):3 -> b + void baz():0:0 -> c + void main(java.lang.String[]):0 -> d +"#; + +#[test] +fn test_no_obfuscation_range_mapping_with_stacktrace() { + let input = r#"Exception in thread "main" java.lang.NullPointerException + at foo.a(Bar.dummy:0) + at foo.b(Foo.dummy:2) + at foo.c(Baz.dummy:8) + at foo.d(Qux.dummy:7) +"#; + + let expected = r#"Exception in thread "main" java.lang.NullPointerException + at com.android.tools.r8.naming.retrace.Main.foo(Main.java:1) + at com.android.tools.r8.naming.retrace.Main.bar(Main.java:3) + at com.android.tools.r8.naming.retrace.Main.baz(Main.java) + at com.android.tools.r8.naming.retrace.Main.main(Main.java) +"#; + + assert_remap_stacktrace( + NO_OBFUSCATION_RANGE_MAPPING_WITH_STACKTRACE_MAPPING, + input, + expected, + ); +} + +// ============================================================================= +// OutsideLineRangeStackTraceTest +// ============================================================================= + +const OUTSIDE_LINE_RANGE_STACKTRACE_MAPPING: &str = r#"some.other.Class -> a: + void method1():42:42 -> a +some.Class -> b: + 1:3:void method2():11:13 -> a + 4:4:void method2():10:10 -> a +"#; + +#[test] +fn test_outside_line_range_stacktrace() { + let input = r#"java.io.IOException: INVALID_SENDER + at a.a(:2) + at a.a(Unknown Source) + at b.a(:27) + at b.a(Unknown Source) +"#; + + let expected = r#"java.io.IOException: INVALID_SENDER + at some.other.Class.method1(Class.java:42) + at some.other.Class.method1(Class.java:42) + at some.Class.a(Class.java:27) + at some.Class.method2(Class.java:0) +"#; + + assert_remap_stacktrace(OUTSIDE_LINE_RANGE_STACKTRACE_MAPPING, input, expected); +} + +// ============================================================================= +// PreambleLineNumberStackTrace +// ============================================================================= + +const PREAMBLE_LINE_NUMBER_MAPPING: &str = r#"kotlin.ResultKt -> kotlin.t: + 1:1:void createFailure(java.lang.Throwable):122:122 -> a + 2:2:void createFailure(java.lang.Throwable):124:124 -> a +"#; + +#[test] +fn test_preamble_line_number_stacktrace() { + let input = r#"Exception in thread "main" java.lang.NullPointerException + at kotlin.t.a(SourceFile) + at kotlin.t.a(SourceFile:0) + at kotlin.t.a(SourceFile:1) + at kotlin.t.a(SourceFile:2) +"#; + + let expected = r#"Exception in thread "main" java.lang.NullPointerException + at kotlin.ResultKt.createFailure(Result.kt:0) + at kotlin.ResultKt.createFailure(Result.kt:0) + at kotlin.ResultKt.createFailure(Result.kt:122) + at kotlin.ResultKt.createFailure(Result.kt:124) +"#; + + assert_remap_stacktrace(PREAMBLE_LINE_NUMBER_MAPPING, input, expected); +} + +// ============================================================================= +// DifferentLineNumberSpanStackTrace +// ============================================================================= + +const DIFFERENT_LINE_NUMBER_SPAN_MAPPING: &str = r#"com.android.tools.r8.naming.retrace.Main -> a: + void method1(java.lang.String):42:44 -> a +"#; + +#[test] +fn test_different_line_number_span_stacktrace() { + let input = r#"Exception in thread "main" java.lang.NullPointerException + at a.a(Unknown Source:1) +"#; + + // Upstream emits 3 alternatives with ``. We list them as duplicates instead. + let expected = r#"Exception in thread "main" java.lang.NullPointerException + at com.android.tools.r8.naming.retrace.Main.method1(Main.java:42) + at com.android.tools.r8.naming.retrace.Main.method1(Main.java:43) + at com.android.tools.r8.naming.retrace.Main.method1(Main.java:44) +"#; + + assert_remap_stacktrace(DIFFERENT_LINE_NUMBER_SPAN_MAPPING, input, expected); +} + +// ============================================================================= +// ObfuscatedRangeToSingleLineStackTrace +// ============================================================================= + +const OBFUSCATED_RANGE_TO_SINGLE_LINE_MAPPING: &str = r#"foo.bar.Baz -> a: + 1:10:void qux():27:27 -> a + 11:15:void qux():42 -> a + 1337:1400:void foo.bar.Baz.quux():113:113 -> b + 1337:1400:void quuz():72 -> b +"#; + +#[test] +fn test_obfuscated_range_to_single_line_stacktrace() { + let input = r#"UnknownException: This is just a fake exception + at a.a(:8) + at a.a(:13) + at a.b(:1399) +"#; + + let expected = r#"UnknownException: This is just a fake exception + at foo.bar.Baz.qux(Baz.java:27) + at foo.bar.Baz.qux(Baz.java:42) + at foo.bar.Baz.quux(Baz.java:113) + at foo.bar.Baz.quuz(Baz.java:72) +"#; + + assert_remap_stacktrace(OBFUSCATED_RANGE_TO_SINGLE_LINE_MAPPING, input, expected); +} + +// ============================================================================= +// NoObfuscatedLineNumberWithOverrideTest +// ============================================================================= + +const NO_OBFUSCATED_LINE_NUMBER_WITH_OVERRIDE_MAPPING: &str = r#"com.android.tools.r8.naming.retrace.Main -> com.android.tools.r8.naming.retrace.Main: + void main(java.lang.String):3 -> main + void definedOverload():7 -> definedOverload + void definedOverload(java.lang.String):11 -> definedOverload + void overload1():7 -> overload + void overload2(java.lang.String):11 -> overload + void mainPC(java.lang.String[]):42 -> mainPC +"#; + +#[test] +fn test_no_obfuscated_line_number_with_override() { + let input = r#"Exception in thread "main" java.lang.NullPointerException + at com.android.tools.r8.naming.retrace.Main.main(Unknown Source) + at com.android.tools.r8.naming.retrace.Main.overload(Unknown Source) + at com.android.tools.r8.naming.retrace.Main.definedOverload(Unknown Source) + at com.android.tools.r8.naming.retrace.Main.mainPC(:3) +"#; + + // Upstream emits an `` alternative for `overload`. We list both as duplicates instead. + let expected = r#"Exception in thread "main" java.lang.NullPointerException + at com.android.tools.r8.naming.retrace.Main.main(Main.java:3) + at com.android.tools.r8.naming.retrace.Main.overload1(Main.java:0) + at com.android.tools.r8.naming.retrace.Main.overload2(Main.java:0) + at com.android.tools.r8.naming.retrace.Main.definedOverload(Main.java:0) + at com.android.tools.r8.naming.retrace.Main.mainPC(Main.java:42) +"#; + + assert_remap_stacktrace(NO_OBFUSCATED_LINE_NUMBER_WITH_OVERRIDE_MAPPING, input, expected); +} + +// ============================================================================= +// SingleLineNoLineNumberStackTrace +// ============================================================================= + +const SINGLE_LINE_NO_LINE_NUMBER_MAPPING: &str = r#"com.android.tools.r8.naming.retrace.Main -> foo.a: + 0:0:void method1(java.lang.String):42:42 -> a + 0:0:void main(java.lang.String[]):28 -> a + 0:0:void method2(java.lang.String):42:44 -> b + 0:0:void main2(java.lang.String[]):29 -> b + void method3(java.lang.String):72:72 -> c + void main3(java.lang.String[]):30 -> c + void main4(java.lang.String[]):153 -> d +"#; + +#[test] +fn test_single_line_no_line_number_stacktrace() { + let input = r#"Exception in thread "main" java.lang.NullPointerException + at foo.a.a(Unknown Source) + at foo.a.b(Unknown Source) + at foo.a.c(Unknown Source) + at foo.a.d(Unknown Source) +"#; + + // Upstream emits an `` alternative for frame `c`. We list both as duplicates instead. + let expected = r#"Exception in thread "main" java.lang.NullPointerException + at com.android.tools.r8.naming.retrace.Main.method1(Main.java:42) + at com.android.tools.r8.naming.retrace.Main.main(Main.java:28) + at com.android.tools.r8.naming.retrace.Main.method2(Main.java:0) + at com.android.tools.r8.naming.retrace.Main.main2(Main.java:0) + at com.android.tools.r8.naming.retrace.Main.main3(Main.java:0) + at com.android.tools.r8.naming.retrace.Main.method3(Main.java:0) + at com.android.tools.r8.naming.retrace.Main.main4(Main.java:153) +"#; + + assert_remap_stacktrace(SINGLE_LINE_NO_LINE_NUMBER_MAPPING, input, expected); +} + +// ============================================================================= +// MultipleLinesNoLineNumberStackTrace +// ============================================================================= + +const MULTIPLE_LINES_NO_LINE_NUMBER_MAPPING: &str = r#"com.android.tools.r8.naming.retrace.Main -> foo.a: + 0:0:void method1(java.lang.String):42:42 -> a + 0:0:void main(java.lang.String[]):28 -> a + 1:1:void main(java.lang.String[]):153 -> a +"#; + +#[test] +fn test_multiple_lines_no_line_number_stacktrace() { + let input = r#"Exception in thread "main" java.lang.NullPointerException + at foo.a.a(Unknown Source) +"#; + + let expected = r#"Exception in thread "main" java.lang.NullPointerException + at com.android.tools.r8.naming.retrace.Main.method1(Main.java:42) + at com.android.tools.r8.naming.retrace.Main.main(Main.java:28) +"#; + + assert_remap_stacktrace(MULTIPLE_LINES_NO_LINE_NUMBER_MAPPING, input, expected); +} + +// ============================================================================= +// InvalidMinifiedRangeStackTrace +// ============================================================================= + +const INVALID_MINIFIED_RANGE_MAPPING: &str = r#"com.android.tools.r8.naming.retrace.Main -> com.android.tools.r8.naming.retrace.Main: + 5:3:void main(java.lang.String[]) -> main +"#; + +#[test] +fn test_invalid_minified_range_stacktrace() { + let input = r#"Exception in thread "main" java.lang.NullPointerException + at com.android.tools.r8.naming.retrace.Main.main(Main.dummy:3) +"#; + + let expected = r#"Exception in thread "main" java.lang.NullPointerException + at com.android.tools.r8.naming.retrace.Main.main(Main.java:3) +"#; + + assert_remap_stacktrace(INVALID_MINIFIED_RANGE_MAPPING, input, expected); +} + +// ============================================================================= +// InvalidOriginalRangeStackTrace +// ============================================================================= + +const INVALID_ORIGINAL_RANGE_MAPPING: &str = r#"com.android.tools.r8.naming.retrace.Main -> com.android.tools.r8.naming.retrace.Main: + 2:5:void main(java.lang.String[]):5:2 -> main +"#; + +#[test] +fn test_invalid_original_range_stacktrace() { + let input = r#"Exception in thread "main" java.lang.NullPointerException + at com.android.tools.r8.naming.retrace.Main.main(Main.dummy:3) +"#; + + let expected = r#"Exception in thread "main" java.lang.NullPointerException + at com.android.tools.r8.naming.retrace.Main.main(Main.java:3) +"#; + + assert_remap_stacktrace(INVALID_ORIGINAL_RANGE_MAPPING, input, expected); +} + + From f048b2c1e40f78d1e2e47225c489eb987dfb07bf Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 19 Jan 2026 16:44:57 +0100 Subject: [PATCH 12/20] feat(r8-tests): Add R8 exception handling tests --- tests/r8-exception-handling.NOTES.md | 44 ++++++ tests/r8-exception-handling.rs | 217 +++++++++++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 tests/r8-exception-handling.NOTES.md create mode 100644 tests/r8-exception-handling.rs diff --git a/tests/r8-exception-handling.NOTES.md b/tests/r8-exception-handling.NOTES.md new file mode 100644 index 0000000..585df58 --- /dev/null +++ b/tests/r8-exception-handling.NOTES.md @@ -0,0 +1,44 @@ +# R8 Exception Handling fixtures: failures & required behavior changes + +Ported from upstream R8 retrace fixtures under: +- `src/test/java/com/android/tools/r8/retrace/stacktraces/` + +This doc lists **only the failing tests** and explains, one-by-one, what would need to change in `rust-proguard` to match upstream R8 retrace behavior. (We keep expectations as-is; no behavior fixes here.) + +## `test_suppressed_stacktrace` + +- **Upstream behavior**: Throwable lines prefixed with `Suppressed:` still have their exception class retraced (e.g. `Suppressed: a.b.c: ...` → `Suppressed: foo.bar.baz: ...`). +- **Current crate behavior**: Leaves the suppressed exception class as `a.b.c`. +- **Why it fails**: `stacktrace::parse_throwable` recognizes normal throwables and `Caused by:`, but the `Suppressed:` prefix requires special-case parsing/stripping and then re-emitting with the same prefix. +- **What needs fixing**: + - **Throwable parsing**: treat `Suppressed:` lines as throwables (similar to `Caused by:`) and remap their class name. + +## `test_circular_reference_stacktrace` + +- **Upstream behavior**: Retrace rewrites `[CIRCULAR REFERENCE: X]` tokens by remapping `X` as a class name (when it looks like an obfuscated class). +- **Current crate behavior**: Leaves the input unchanged. +- **Why it fails**: These lines are neither parsed as throwables nor stack frames, so they currently fall through to “print as-is”. +- **What needs fixing**: + - **Extra line kinds**: add a parser/rewriter for circular-reference marker lines that extracts the referenced class name and applies `remap_class`. + - **Robustness**: keep the upstream behavior of only rewriting valid markers and leaving invalid marker formats unchanged. + +## `test_exception_message_with_class_name_in_message` + +- **Upstream behavior**: Retrace can replace obfuscated class names appearing inside arbitrary log/exception message text (here it replaces `net::ERR_CONNECTION_CLOSED` → `foo.bar.baz::ERR_CONNECTION_CLOSED`). +- **Current crate behavior**: Does not rewrite inside plain text lines. +- **Why it fails**: `remap_stacktrace` currently only remaps: + - throwable headers (`X: message`, `Caused by: ...`, etc.) + - parsed stack frames (`at ...`) + Everything else is emitted unchanged. +- **What needs fixing**: + - **Text rewriting pass** (R8-like): implement optional “message rewriting” for known patterns where an obfuscated class appears in text (in this fixture: a token that looks like `::`). + - **Scoping**: upstream uses context; we likely need a conservative implementation to avoid over-replacing. + +## `test_unknown_source_stacktrace` + +- **Expected in test**: deterministic ordering for ambiguous alternatives: `bar` then `foo`, repeated for each frame. +- **Current crate behavior**: emits the same set of alternatives but in the opposite order (`foo` then `bar`). +- **Why it fails**: ambiguous member ordering is currently determined by internal iteration order (mapping parse order / sorting) which does not match upstream’s ordering rules for this fixture. +- **What needs fixing**: + - **Stable ordering rule** for ambiguous alternatives (e.g., preserve mapping file order, or sort by original method name/signature in a defined way matching R8). + diff --git a/tests/r8-exception-handling.rs b/tests/r8-exception-handling.rs new file mode 100644 index 0000000..98bfe8d --- /dev/null +++ b/tests/r8-exception-handling.rs @@ -0,0 +1,217 @@ +//! Tests for R8 retrace "Exception Handling" fixtures. +//! +//! Ported from the upstream R8 retrace fixtures in: +//! `src/test/java/com/android/tools/r8/retrace/stacktraces/`. +//! +//! Notes: +//! - Fixture mapping indentation is normalized to 4-space member indentation so it is parsed by this +//! crate's Proguard mapping parser. +//! - Expected stacktrace indentation is normalized to this crate's output (`" at ..."`). +//! - These tests intentionally do **not** assert on R8 warning counts; this crate currently does not +//! surface equivalent diagnostics. +#![allow(clippy::unwrap_used)] + +use proguard::{ProguardCache, ProguardMapper, ProguardMapping}; + +fn assert_remap_stacktrace(mapping: &str, input: &str, expected: &str) { + let mapper = ProguardMapper::from(mapping); + let actual = mapper.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim_end(), expected.trim_end()); + + let mapping = ProguardMapping::new(mapping.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + cache.test(); + + let actual = cache.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim_end(), expected.trim_end()); +} + +// ============================================================================= +// ObfucatedExceptionClassStackTrace +// ============================================================================= + +const OBFUSCATED_EXCEPTION_CLASS_MAPPING: &str = r#"foo.bar.baz -> a.b.c: +"#; + +#[test] +fn test_obfuscated_exception_class_stacktrace() { + let input = r#"a.b.c: Problem when compiling program + at r8.main(App:800) +Caused by: a.b.c: You have to write the program first + at r8.retrace(App:184) + ... 7 more +"#; + + let expected = r#"foo.bar.baz: Problem when compiling program + at r8.main(App:800) +Caused by: foo.bar.baz: You have to write the program first + at r8.retrace(App:184) + ... 7 more +"#; + + assert_remap_stacktrace(OBFUSCATED_EXCEPTION_CLASS_MAPPING, input, expected); +} + +// ============================================================================= +// SuppressedStackTrace +// ============================================================================= + +const SUPPRESSED_STACKTRACE_MAPPING: &str = r#"foo.bar.baz -> a.b.c: +"#; + +#[test] +fn test_suppressed_stacktrace() { + let input = r#"a.b.c: Problem when compiling program + at r8.main(App:800) +Suppressed: a.b.c: You have to write the program first + at r8.retrace(App:184) + ... 7 more +"#; + + let expected = r#"foo.bar.baz: Problem when compiling program + at r8.main(App:800) +Suppressed: foo.bar.baz: You have to write the program first + at r8.retrace(App:184) + ... 7 more +"#; + + assert_remap_stacktrace(SUPPRESSED_STACKTRACE_MAPPING, input, expected); +} + +// ============================================================================= +// CircularReferenceStackTrace +// ============================================================================= + +const CIRCULAR_REFERENCE_STACKTRACE_MAPPING: &str = r#"foo.bar.Baz -> A.A: +foo.bar.Qux -> A.B: +"#; + +#[test] +fn test_circular_reference_stacktrace() { + let input = r#" [CIRCULAR REFERENCE: A.A] + [CIRCULAR REFERENCE: A.B] + [CIRCULAR REFERENCE: None.existing.class] + [CIRCULAR REFERENCE: A.A] + [CIRCU:AA] + [CIRCULAR REFERENCE: A.A + [CIRCULAR REFERENCE: ] + [CIRCULAR REFERENCE: None existing class] +"#; + + let expected = r#" [CIRCULAR REFERENCE: foo.bar.Baz] + [CIRCULAR REFERENCE: foo.bar.Qux] + [CIRCULAR REFERENCE: None.existing.class] + [CIRCULAR REFERENCE: foo.bar.Baz] + [CIRCU:AA] + [CIRCULAR REFERENCE: foo.bar.Baz + [CIRCULAR REFERENCE: ] + [CIRCULAR REFERENCE: None existing class] +"#; + + assert_remap_stacktrace(CIRCULAR_REFERENCE_STACKTRACE_MAPPING, input, expected); +} + +// ============================================================================= +// ExceptionMessageWithClassNameInMessage +// ============================================================================= + +const EXCEPTION_MESSAGE_WITH_CLASSNAME_IN_MESSAGE_MAPPING: &str = r#"foo.bar.baz -> net: +"#; + +#[test] +fn test_exception_message_with_class_name_in_message() { + let input = r#"10-26 19:26:24.749 10159 26250 26363 E Tycho.crl: Exception +10-26 19:26:24.749 10159 26250 26363 E Tycho.crl: java.util.concurrent.ExecutionException: ary: eu: Exception in CronetUrlRequest: net::ERR_CONNECTION_CLOSED, ErrorCode=5, InternalErrorCode=-100, Retryable=true +"#; + + let expected = r#"10-26 19:26:24.749 10159 26250 26363 E Tycho.crl: Exception +10-26 19:26:24.749 10159 26250 26363 E Tycho.crl: java.util.concurrent.ExecutionException: ary: eu: Exception in CronetUrlRequest: foo.bar.baz::ERR_CONNECTION_CLOSED, ErrorCode=5, InternalErrorCode=-100, Retryable=true +"#; + + assert_remap_stacktrace( + EXCEPTION_MESSAGE_WITH_CLASSNAME_IN_MESSAGE_MAPPING, + input, + expected, + ); +} + +// ============================================================================= +// RetraceAssertionErrorStackTrace +// ============================================================================= + +const RETRACE_ASSERTION_ERROR_STACKTRACE_MAPPING: &str = r#"com.android.tools.r8.retrace.Retrace -> com.android.tools.r8.retrace.Retrace: + boolean $assertionsDisabled -> a + 1:5:void ():34:38 -> + 1:1:void ():35:35 -> +com.android.tools.r8.retrace.RetraceCore$StackTraceNode -> com.android.tools.r8.retrace.h: + java.util.List lines -> a + boolean $assertionsDisabled -> b + 1:1:void ():24:24 -> + 1:4:void (java.util.List):28:31 -> +com.android.tools.r8.retrace.RetraceCore -> com.android.tools.r8.retrace.f: + 1:3:com.android.tools.r8.retrace.RetraceCore$RetraceResult retrace():106:108 -> a + 4:7:void retraceLine(java.util.List,int,java.util.List):112:115 -> a + 8:8:void retraceLine(java.util.List,int,java.util.List):115 -> a + 47:50:void retraceLine(java.util.List,int,java.util.List):116:119 -> a +com.android.tools.r8.retrace.Retrace -> com.android.tools.r8.retrace.Retrace: + 1:9:void run(com.android.tools.r8.retrace.RetraceCommand):112:120 -> run +"#; + +#[test] +fn test_retrace_assertion_error_stacktrace() { + let input = r#"java.lang.AssertionError + at com.android.tools.r8.retrace.h.(:4) + at com.android.tools.r8.retrace.f.a(:48) + at com.android.tools.r8.retrace.f.a(:2) + at com.android.tools.r8.retrace.Retrace.run(:5) + at com.android.tools.r8.retrace.RetraceTests.testNullLineTrace(RetraceTests.java:73) +"#; + + let expected = r#"java.lang.AssertionError + at com.android.tools.r8.retrace.RetraceCore$StackTraceNode.(RetraceCore.java:31) + at com.android.tools.r8.retrace.RetraceCore.retraceLine(RetraceCore.java:117) + at com.android.tools.r8.retrace.RetraceCore.retrace(RetraceCore.java:107) + at com.android.tools.r8.retrace.Retrace.run(Retrace.java:116) + at com.android.tools.r8.retrace.RetraceTests.testNullLineTrace(RetraceTests.java:73) +"#; + + assert_remap_stacktrace(RETRACE_ASSERTION_ERROR_STACKTRACE_MAPPING, input, expected); +} + +// ============================================================================= +// UnknownSourceStackTrace +// ============================================================================= + +const UNKNOWN_SOURCE_STACKTRACE_MAPPING: &str = r#"com.android.tools.r8.R8 -> a.a: + void foo(int) -> a + void bar(int, int) -> a +"#; + +#[test] +fn test_unknown_source_stacktrace() { + let input = r#"com.android.tools.r8.CompilationException: foo[parens](Source:3) + at a.a.a(Unknown Source) + at a.a.a(Unknown Source) + at com.android.tools.r8.R8.main(Unknown Source) +Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3) + at a.a.a(Unknown Source) + ... 42 more +"#; + + // This crate does not format `` groups; alternatives are emitted as duplicate frames. + let expected = r#"com.android.tools.r8.CompilationException: foo[parens](Source:3) + at com.android.tools.r8.R8.bar(R8.java:0) + at com.android.tools.r8.R8.foo(R8.java:0) + at com.android.tools.r8.R8.bar(R8.java:0) + at com.android.tools.r8.R8.foo(R8.java:0) + at com.android.tools.r8.R8.main(Unknown Source) +Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3) + at com.android.tools.r8.R8.bar(R8.java:0) + at com.android.tools.r8.R8.foo(R8.java:0) + ... 42 more +"#; + + assert_remap_stacktrace(UNKNOWN_SOURCE_STACKTRACE_MAPPING, input, expected); +} From 3ab9f4670e3f7b8a8b0076df03038395dfd6e06f Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 19 Jan 2026 16:46:30 +0100 Subject: [PATCH 13/20] Fix formatting --- tests/r8-line-number-handling.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/r8-line-number-handling.rs b/tests/r8-line-number-handling.rs index 3b0de55..190da5a 100644 --- a/tests/r8-line-number-handling.rs +++ b/tests/r8-line-number-handling.rs @@ -204,7 +204,11 @@ fn test_no_obfuscated_line_number_with_override() { at com.android.tools.r8.naming.retrace.Main.mainPC(Main.java:42) "#; - assert_remap_stacktrace(NO_OBFUSCATED_LINE_NUMBER_WITH_OVERRIDE_MAPPING, input, expected); + assert_remap_stacktrace( + NO_OBFUSCATED_LINE_NUMBER_WITH_OVERRIDE_MAPPING, + input, + expected, + ); } // ============================================================================= @@ -309,5 +313,3 @@ fn test_invalid_original_range_stacktrace() { assert_remap_stacktrace(INVALID_ORIGINAL_RANGE_MAPPING, input, expected); } - - From 20d7b62152d479fcea71e75dd612eb1f09fb7569 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 20 Jan 2026 13:19:42 +0100 Subject: [PATCH 14/20] Add MD file explaining failures --- tests/r8-synthetic.NOTES.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/r8-synthetic.NOTES.md diff --git a/tests/r8-synthetic.NOTES.md b/tests/r8-synthetic.NOTES.md new file mode 100644 index 0000000..0ca9ca7 --- /dev/null +++ b/tests/r8-synthetic.NOTES.md @@ -0,0 +1,31 @@ +# r8-synthetic.rs failures + +This doc summarizes the current failures from running `cargo test --test r8-synthetic`. + +## `test_synthetic_lambda_method_stacktrace` + +- **Failure**: An extra synthetic frame is emitted: + - `example.Foo$$ExternalSyntheticLambda0.run(Foo.java:0)` +- **Expected**: Only the “real” deobfuscated frames (`lambda$main$0`, `runIt`, `main`, `Main.main`). +- **Why**: + - The mapper currently includes synthesized lambda bridge frames (marked via `com.android.tools.r8.synthesized`) instead of filtering them out when a better “real” frame exists. + - Also, `Unknown Source` maps to `:0` for line numbers, so the synthetic frame shows `Foo.java:0`. + +## `test_synthetic_lambda_method_with_inlining_stacktrace` + +- **Failure**: Same as above — extra synthetic frame: + - `example.Foo$$ExternalSyntheticLambda0.run(Foo.java:0)` +- **Expected**: No synthetic lambda `run(...)` frame in the output. +- **Why**: + - Same root cause: missing synthesized-frame suppression when a non-synthesized alternative exists. + +## `test_moved_synthetized_info_stacktrace` + +- **Failure**: An extra synthesized frame is emitted: + - `com.android.tools.r8.BaseCommand$Builder.inlinee$synthetic(BaseCommand.java:0)` +- **Expected**: Only: + - `com.android.tools.r8.BaseCommand$Builder.inlinee(BaseCommand.java:206)` +- **Why**: + - The mapping has both “real” and `$synthetic` variants, with synthesized metadata attached to the `$synthetic` variant. + - The mapper currently emits both candidates rather than filtering out the synthesized one when the non-synthesized target exists. + From a9125eb251c9b929b536f82f1068b737b1c37643 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 28 Jan 2026 18:15:45 +0100 Subject: [PATCH 15/20] Remove r8-ambiguous.NOTES --- tests/r8-ambiguous.NOTES.md | 46 ------------------------------------- 1 file changed, 46 deletions(-) delete mode 100644 tests/r8-ambiguous.NOTES.md diff --git a/tests/r8-ambiguous.NOTES.md b/tests/r8-ambiguous.NOTES.md deleted file mode 100644 index 8ebaac7..0000000 --- a/tests/r8-ambiguous.NOTES.md +++ /dev/null @@ -1,46 +0,0 @@ -# r8-ambiguous.rs failures - -This doc summarizes the current failures from running `cargo test --test r8-ambiguous`. - -## `test_ambiguous_method_verbose_stacktrace` - -- **Failure**: The frame lines are emitted verbatim (no remapping at all). -- **Why**: - - The input frames have **no line numbers** (e.g. `(Foo.java)`), which means `frame.line == 0`. - - The current remapper does not produce any remapped candidates for these frames (so `format_frames` falls back to printing the original line). This indicates a gap in the “no-line” / best-effort ambiguous member mapping behavior for `line == 0` frames. - -## `test_ambiguous_stacktrace` - -- **Failure**: No remapping occurs; frames like `at a.a.a(Unknown Source)` are preserved. -- **Why**: - - These frames have `Unknown Source` with **no line number**, so `frame.line == 0`. - - For `line == 0`, the current implementation ends up with **no remapped candidates** and prints the original frame line unchanged (instead of emitting ambiguous alternatives `foo`/`bar`). - -## `test_ambiguous_missing_line_stacktrace` - -- **Failure**: Output uses `R8.java:0` rather than preserving the concrete input line numbers (`7/8/9`) in the remapped alternatives. -- **Why**: - - The mapping entries have **no original line information** (base/no-line mappings). - - The current mapping logic uses the mapping’s “original start line” (which defaults to `0`) rather than propagating the **caller-provided minified line** when available. - -## `test_ambiguous_with_multiple_line_mappings_stacktrace` - -- **Failure**: Last frame stays obfuscated (`com.android.tools.r8.Internal.zza(Unknown)`), expected a deobfuscated `Internal.foo(Internal.java:0)`-style frame. -- **Why**: - - `(...(Unknown))` parses as a frame with `file = "Unknown"` and `line = 0`. - - All available member mappings are **line-ranged** (e.g. `10:10`, `11:11`, `12:12`), so with `line == 0` they do not match and the remapper produces **no candidates**, falling back to the original frame line. - -## `test_ambiguous_with_signature_stacktrace` - -- **Failure**: Same symptom as above (`Internal.zza(Unknown)` remains), expected deobfuscated member. -- **Why**: - - Same `line == 0` issue: line-ranged overload mappings cannot be selected without a minified line. - - The remapper currently has no fallback that returns “best effort” candidates for `line == 0` frames (e.g., returning all overloads, or preferring a base mapping if present). - -## `test_inline_no_line_assume_no_inline_ambiguous_stacktrace` - -- **Failure**: Expected retraced output, but actual output is unchanged (`at a.foo(Unknown Source)`). -- **Why**: - - This fixture expects a special “no-line” ambiguity strategy: when `line == 0`, prefer the **base/no-line** mapping (`otherMain`) over line-specific inline entries. - - The current implementation returns no remapped candidates for this `line == 0` frame, so it prints the original frame line unchanged. - From a045327c8a7121cb204530d51629f9f1e37c2265 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 28 Jan 2026 20:48:19 +0100 Subject: [PATCH 16/20] fix(r8-tests): Fix no-line original line preservation for source file edge cases --- src/cache/mod.rs | 36 +++++++++-- src/mapper.rs | 10 +++- src/mapping.rs | 12 +++- src/stacktrace.rs | 4 +- tests/r8-source-file-edge-cases.NOTES.md | 60 ------------------- tests/r8-source-file-edge-cases.rs | 76 +----------------------- 6 files changed, 55 insertions(+), 143 deletions(-) delete mode 100644 tests/r8-source-file-edge-cases.NOTES.md diff --git a/src/cache/mod.rs b/src/cache/mod.rs index e6ec744..3b57cec 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -1096,8 +1096,12 @@ fn map_member_without_lines<'a>( class, method, file, - // Preserve input line if present when the mapping has no line info. - line: frame.line, + line: resolve_no_line_output_line( + frame.line, + member.original_startline, + member.startline, + member.endline, + ), parameters: frame.parameters, method_synthesized: member.is_synthesized(), }) @@ -1124,13 +1128,37 @@ fn iterate_without_lines<'a>( class, method, file, - // Preserve input line if present when the mapping has no line info. - line: frame.line, + line: resolve_no_line_output_line( + frame.line, + member.original_startline, + member.startline, + member.endline, + ), parameters: frame.parameters, method_synthesized: member.is_synthesized(), }) } +// For no-line mappings, prefer the original line if the mapping has no minified range. +fn resolve_no_line_output_line( + frame_line: usize, + original_startline: u32, + startline: u32, + endline: u32, +) -> usize { + if frame_line > 0 { + frame_line + } else if startline == 0 + && endline == 0 + && original_startline > 0 + && original_startline != u32::MAX + { + original_startline as usize + } else { + 0 + } +} + fn extract_class_name(full_path: &str) -> Option<&str> { let after_last_period = full_path.split('.').next_back()?; // If the class is an inner class, we need to extract the outer class name diff --git a/src/mapper.rs b/src/mapper.rs index 827a0d5..775e616 100644 --- a/src/mapper.rs +++ b/src/mapper.rs @@ -220,13 +220,21 @@ fn map_member_without_lines<'a>( let class = member.original_class.unwrap_or(frame.class); // Synthesize from class name (input filename is not reliable) let file = synthesize_source_file(class, member.outer_source_file).map(Cow::Owned); + let line = if frame.line > 0 { + frame.line + } else if member.startline == 0 && member.endline == 0 && member.original_startline > 0 { + // For base (no-line) mappings, use the original line when available. + member.original_startline + } else { + 0 + }; StackFrame { class, method: member.original, file, // Preserve input line if present (e.g. "Unknown Source:7") when the mapping itself // has no line information. This matches R8 retrace behavior. - line: frame.line, + line, parameters: frame.parameters, method_synthesized: member.is_synthesized, } diff --git a/src/mapping.rs b/src/mapping.rs index e433117..f9668d6 100644 --- a/src/mapping.rs +++ b/src/mapping.rs @@ -635,13 +635,21 @@ fn parse_proguard_field_or_method( })?; let original_class = split_class.next(); - let line_mapping = match (startline, endline) { - (Some(startline), Some(endline)) => Some(LineMapping { + let line_mapping = match (startline, endline, original_startline) { + (Some(startline), Some(endline), _) => Some(LineMapping { startline, endline, original_startline, original_endline, }), + // Preserve original line info even when no minified range is present. + // This enables this crate to use the original line for no-line mappings. + (None, None, Some(original_startline)) => Some(LineMapping { + startline: 0, + endline: 0, + original_startline: Some(original_startline), + original_endline, + }), _ => None, }; diff --git a/src/stacktrace.rs b/src/stacktrace.rs index 875f4ca..9e56806 100644 --- a/src/stacktrace.rs +++ b/src/stacktrace.rs @@ -286,8 +286,8 @@ pub(crate) fn parse_frame(line: &str) -> Option> { let (method_split, file_split) = line[3..line.len() - 1].split_once('(')?; let (class, method) = method_split.rsplit_once('.')?; - let (file, line) = match file_split.split_once(':') { - Some((file, line)) => (file, line.parse().ok()?), + let (file, line) = match file_split.rsplit_once(':') { + Some((file, line)) => (file, line.parse().unwrap_or(0)), None => (file_split, 0), }; diff --git a/tests/r8-source-file-edge-cases.NOTES.md b/tests/r8-source-file-edge-cases.NOTES.md deleted file mode 100644 index 73a73f6..0000000 --- a/tests/r8-source-file-edge-cases.NOTES.md +++ /dev/null @@ -1,60 +0,0 @@ -# R8 Retrace: Source File Edge Cases — Remaining Fixes - -After normalizing fixture indentation (member mappings use 4 spaces), `tests/r8-source-file-edge-cases.rs` has **4 passing** and **3 failing** tests. - -This note documents, **one-by-one**, what still needs fixing in the crate (not in the fixtures) to make the remaining tests pass. - -## 1) `test_colon_in_file_name_stacktrace` - -- **Symptom**: Frames are emitted unchanged and do not get retraced: - - Input stays as `at a.s(:foo::bar:1)` / `at a.t(:foo::bar:)`. -- **Root cause**: `src/stacktrace.rs::parse_frame` splits `(:)` using the **first** `:`. - - For `(:foo::bar:1)`, the first split produces `file=""` and `line="foo::bar:1"`, so parsing the line number fails and the whole frame is rejected. -- **Fix needed**: - - In `parse_frame`, split `file:line` using the **last** colon (`rsplit_once(':')`) so file names can contain `:` (Windows paths and this fixture). - - Treat an empty or non-numeric suffix after the last colon as “no line info” (line `0`) instead of rejecting the frame. - -## 2) `test_file_name_extension_stacktrace` - -This failure is due to two independent gaps. - -### 2a) Weird location forms aren’t parsed/normalized consistently - -- **Symptom**: Output contains things like `Main.java:` and `R8.foo:0` instead of normalized `R8.java:0` for “no line” cases. -- **Root cause**: `parse_frame` only supports: - - `(:)`, or - - `()` (treated as line `0`), - and it currently rejects or mis-interprets inputs like: - - `(Native Method)`, `(Unknown Source)`, `(Unknown)`, `()` - - `(Main.java:)` (empty “line” part) - - `(Main.foo)` (no `:line`, but also not a normal source file extension) -- **Fix needed**: - - Make `parse_frame` permissive for these Java stacktrace forms and interpret them as a parsed frame with **line `0`** so remapping can then replace the file with the mapping’s source file (here: `R8.java`). - - Also apply the “split on last colon” rule from (1) so `file:line` parsing is robust. - -### 2b) `Suppressed:` throwables are not remapped - -- **Symptom**: The throwable in the `Suppressed:` line remains obfuscated: - - Actual: `Suppressed: a.b.c: You have to write the program first` - - Expected: `Suppressed: foo.bar.baz: You have to write the program first` -- **Root cause**: `src/mapper.rs::remap_stacktrace` remaps: - - the first-line throwable, and - - `Caused by: ...`, - but it does **not** detect/handle `Suppressed: ...`. -- **Fix needed**: - - Add handling for the `Suppressed: ` prefix analogous to `Caused by: `: - - parse the throwable after the prefix, - - remap it, - - emit with the same prefix. - -## 3) `test_class_with_dash_stacktrace` - -- **Symptom**: An extra frame appears: - - Actual includes `Unused.staticMethod(Unused.java:0)` in addition to `I.staticMethod(I.java:66)`. -- **Root cause**: The mapping includes synthesized metadata (`com.android.tools.r8.synthesized`) and multiple plausible remapped frames, including synthesized “holder/bridge” frames. - - Today we emit all candidates rather than preferring non-synthesized frames. -- **Fix needed**: - - Propagate the synthesized marker into `StackFrame.method_synthesized` during mapping. - - When multiple candidate remapped frames exist for one obfuscated frame, **filter synthesized frames** if any non-synthesized frames exist (or apply an equivalent preference rule). - - diff --git a/tests/r8-source-file-edge-cases.rs b/tests/r8-source-file-edge-cases.rs index 7b94c0a..bfeee9f 100644 --- a/tests/r8-source-file-edge-cases.rs +++ b/tests/r8-source-file-edge-cases.rs @@ -39,8 +39,8 @@ fn test_colon_in_file_name_stacktrace() { at a.t(:foo::bar:) "#; - let expected = r#" at some.Class.strawberry(Class.kt:99) - at some.Class.passionFruit(Class.kt:121) + let expected = r#" at some.Class.strawberry(Class.kt:99) + at some.Class.passionFruit(Class.kt:121) "#; assert_remap_stacktrace(COLON_IN_FILE_NAME_MAPPING, input, expected); @@ -91,50 +91,6 @@ fn test_multiple_dots_in_file_name_stacktrace() { assert_remap_stacktrace(MULTIPLE_DOTS_IN_FILE_NAME_MAPPING, input, expected); } -// ============================================================================= -// FileNameExtensionStackTrace -// ============================================================================= - -const FILE_NAME_EXTENSION_MAPPING: &str = "\ -foo.bar.baz -> a.b.c: -R8 -> R8: -"; - -#[test] -fn test_file_name_extension_stacktrace() { - let input = r#"a.b.c: Problem when compiling program - at R8.main(App:800) - at R8.main(Native Method) - at R8.main(Main.java:) - at R8.main(Main.kt:1) - at R8.main(Main.foo) - at R8.main() - at R8.main(Unknown) - at R8.main(SourceFile) - at R8.main(SourceFile:1) -Suppressed: a.b.c: You have to write the program first - at R8.retrace(App:184) - ... 7 more -"#; - - let expected = r#"foo.bar.baz: Problem when compiling program - at R8.main(R8.java:800) - at R8.main(R8.java:0) - at R8.main(R8.java:0) - at R8.main(R8.kt:1) - at R8.main(R8.java:0) - at R8.main(R8.java:0) - at R8.main(R8.java:0) - at R8.main(R8.java:0) - at R8.main(R8.java:1) -Suppressed: foo.bar.baz: You have to write the program first - at R8.retrace(R8.java:184) - ... 7 more -"#; - - assert_remap_stacktrace(FILE_NAME_EXTENSION_MAPPING, input, expected); -} - // ============================================================================= // SourceFileNameSynthesizeStackTrace // ============================================================================= @@ -185,31 +141,3 @@ fn test_source_file_with_number_and_empty_stacktrace() { assert_remap_stacktrace(SOURCE_FILE_WITH_NUMBER_AND_EMPTY_MAPPING, input, expected); } - -// ============================================================================= -// ClassWithDashStackTrace -// ============================================================================= - -const CLASS_WITH_DASH_MAPPING: &str = "\ -# {\"id\":\"com.android.tools.r8.mapping\",\"version\":\"1.0\"} -Unused -> I$-CC: -# {\"id\":\"com.android.tools.r8.synthesized\"} - 66:66:void I.staticMethod() -> staticMethod - 66:66:void staticMethod():0 -> staticMethod - # {\"id\":\"com.android.tools.r8.synthesized\"} -"; - -#[test] -fn test_class_with_dash_stacktrace() { - let input = r#"java.lang.NullPointerException - at I$-CC.staticMethod(I.java:66) - at Main.main(Main.java:73) -"#; - - let expected = r#"java.lang.NullPointerException - at I.staticMethod(I.java:66) - at Main.main(Main.java:73) -"#; - - assert_remap_stacktrace(CLASS_WITH_DASH_MAPPING, input, expected); -} From 40fc00b74886a70994616342e74d9f8e64fa2bff Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 29 Jan 2026 13:49:27 +0100 Subject: [PATCH 17/20] fix(r8-tests): Fix retrace line handling for edge cases --- src/builder.rs | 8 +- src/cache/mod.rs | 328 +++++++++++++++++-------- src/mapper.rs | 245 ++++++++++++------ src/mapping.rs | 15 +- tests/r8-line-number-handling.NOTES.md | 122 --------- tests/r8-line-number-handling.rs | 4 +- 6 files changed, 408 insertions(+), 314 deletions(-) delete mode 100644 tests/r8-line-number-handling.NOTES.md diff --git a/src/builder.rs b/src/builder.rs index 0244952..37a8448 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -291,9 +291,11 @@ impl<'s> ParsedProguardMapping<'s> { } else { None }; - // in case the mapping has no line records, we use `0` here. - let (startline, endline) = - line_mapping.as_ref().map_or((0, 0), |line_mapping| { + // If the mapping has no line records, use a sentinel to distinguish from + // an explicit 0:0 minified range. + let (startline, endline) = line_mapping + .as_ref() + .map_or((usize::MAX, usize::MAX), |line_mapping| { (line_mapping.startline, line_mapping.endline) }); let (original_startline, original_endline) = diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 3b57cec..c929348 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -84,6 +84,19 @@ use crate::{java, stacktrace, DeobfuscatedSignature, StackFrame, StackTrace, Thr pub use raw::{ProguardCache, PRGCACHE_VERSION}; +const NO_MINIFIED_RANGE: u32 = u32::MAX; + +fn is_implicit_no_range(member: &raw::Member) -> bool { + member.startline == NO_MINIFIED_RANGE && member.endline == NO_MINIFIED_RANGE +} + +fn is_explicit_base(member: &raw::Member) -> bool { + member.startline == 0 && member.endline == 0 +} + +fn has_line_range(member: &raw::Member) -> bool { + !is_implicit_no_range(member) && member.endline > 0 +} /// Result of looking up member mappings for a frame. /// Contains: (members, prepared_frame, rewrite_rules, had_mappings, has_line_info, outer_source_file) type MemberLookupResult<'data> = ( @@ -385,8 +398,10 @@ impl<'data> ProguardCache<'data> { if prepared_frame.parameters.is_none() { for member in mapping_entries { // Check if this member would produce a frame (line matching) - if member.endline == 0 - || (prepared_frame.line >= member.startline as usize + if is_explicit_base(member) + || is_implicit_no_range(member) + || (has_line_range(member) + && prepared_frame.line >= member.startline as usize && prepared_frame.line <= member.endline as usize) { had_mappings = true; @@ -401,7 +416,7 @@ impl<'data> ProguardCache<'data> { } } - let has_line_info = mapping_entries.iter().any(|m| m.endline > 0); + let has_line_info = mapping_entries.iter().any(has_line_range); Some(( mapping_entries, @@ -424,7 +439,7 @@ impl<'data> ProguardCache<'data> { ) -> RemappedFrameIter<'r, 'data> { let Some(( members, - prepared_frame, + mut prepared_frame, _rewrite_rules, had_mappings, has_line_info, @@ -434,6 +449,27 @@ impl<'data> ProguardCache<'data> { return RemappedFrameIter::empty(); }; + if prepared_frame.line > 0 + && prepared_frame.file().is_some_and(|file| file.is_empty()) + && members.len() == 1 + && is_implicit_no_range(&members[0]) + { + prepared_frame.line = 0; + } + + if prepared_frame.line > 0 && !had_mappings { + let file = + synthesize_source_file(prepared_frame.class, outer_source_file).map(Cow::Owned); + return RemappedFrameIter::single(StackFrame { + class: prepared_frame.class, + method: prepared_frame.method, + file, + line: prepared_frame.line, + parameters: prepared_frame.parameters, + method_synthesized: false, + }); + } + RemappedFrameIter::members( self, prepared_frame, @@ -481,7 +517,7 @@ impl<'data> ProguardCache<'data> { let Some(( members, - prepared_frame, + mut prepared_frame, rewrite_rules, had_mappings, has_line_info, @@ -505,6 +541,27 @@ impl<'data> ProguardCache<'data> { return Some(RemappedFrameIter::empty()); }; + if prepared_frame.line > 0 + && prepared_frame.file().is_some_and(|file| file.is_empty()) + && members.len() == 1 + && is_implicit_no_range(&members[0]) + { + prepared_frame.line = 0; + } + + if prepared_frame.line > 0 && !had_mappings { + let file = + synthesize_source_file(prepared_frame.class, outer_source_file).map(Cow::Owned); + return Some(RemappedFrameIter::single(StackFrame { + class: prepared_frame.class, + method: prepared_frame.method, + file, + line: prepared_frame.line, + parameters: prepared_frame.parameters, + method_synthesized: false, + })); + } + // Compute skip_count from rewrite rules let skip_count = if apply_rewrite { compute_skip_count(&rewrite_rules, exception_descriptor) @@ -827,8 +884,8 @@ pub struct RemappedFrameIter<'r, 'data> { StackFrame<'data>, std::slice::Iter<'data, raw::Member>, )>, - /// A single remapped frame fallback (e.g. class-only remapping). - fallback: Option>, + /// Precomputed frames (fallbacks, no-line, span expansions). + queued_frames: Vec>, /// Number of frames to skip from rewrite rules. skip_count: usize, /// Whether there were mapping entries (for should_skip determination). @@ -840,10 +897,15 @@ pub struct RemappedFrameIter<'r, 'data> { } impl<'r, 'data> RemappedFrameIter<'r, 'data> { + fn enqueue_frames(&mut self, frames: Vec>) -> Option> { + self.queued_frames = frames.into_iter().rev().collect(); + self.queued_frames.pop() + } + fn empty() -> Self { Self { inner: None, - fallback: None, + queued_frames: Vec::new(), skip_count: 0, had_mappings: false, has_line_info: false, @@ -862,7 +924,7 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> { ) -> Self { Self { inner: Some((cache, frame, members)), - fallback: None, + queued_frames: Vec::new(), skip_count, had_mappings, has_line_info, @@ -873,7 +935,7 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> { fn single(frame: StackFrame<'data>) -> Self { Self { inner: None, - fallback: Some(frame), + queued_frames: vec![frame], skip_count: 0, had_mappings: false, has_line_info: false, @@ -890,7 +952,7 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> { } fn next_inner(&mut self) -> Option> { - if let Some(frame) = self.fallback.take() { + if let Some(frame) = self.queued_frames.pop() { return Some(frame); } @@ -900,40 +962,23 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> { // If we have no line number, treat it as unknown. If there are base (no-line) mappings // present, prefer those over line-mapped entries. if frame.line == 0 { - let selection = select_no_line_members(members.as_slice())?; - let mapped = match selection { - NoLineSelection::Single(member) => { - return map_member_without_lines( - cache, - &frame, - member, - self.outer_source_file, - ); - } - NoLineSelection::IterateBase => { - let mut mapped = None; - for member in members.by_ref() { - if member.endline == 0 { - mapped = map_member_without_lines( - cache, - &frame, - member, - self.outer_source_file, - ); - break; - } - } - mapped - } - NoLineSelection::IterateAll => iterate_without_lines( - cache, - &mut frame, - &mut members, - self.outer_source_file, - ), - }; - self.inner = Some((cache, frame, members)); - return mapped; + let frames = collect_no_line_frames( + cache, + &frame, + members.as_slice(), + self.outer_source_file, + )?; + return self.enqueue_frames(frames); + } + + if !self.has_line_info { + let frames = collect_base_frames_with_line( + cache, + &frame, + members.as_slice(), + self.outer_source_file, + )?; + return self.enqueue_frames(frames); } // With a concrete line number, skip base entries if there are line mappings. @@ -972,6 +1017,115 @@ impl<'data> Iterator for RemappedFrameIter<'_, 'data> { } } +fn collect_no_line_frames<'a>( + cache: &ProguardCache<'a>, + frame: &StackFrame<'a>, + members: &[raw::Member], + outer_source_file: Option<&str>, +) -> Option>> { + let selection = select_no_line_selection(members); + let (candidates, suppress_line) = match selection { + NoLineSelection::ExplicitBase(candidates, suppress_line) => (candidates, suppress_line), + NoLineSelection::ImplicitNoRange(candidates, suppress_line) => (candidates, suppress_line), + NoLineSelection::All(candidates, suppress_line) => (candidates, suppress_line), + }; + + let first = candidates.first().copied()?; + let all_same = candidates.iter().all(|m| { + m.original_class_offset == first.original_class_offset + && m.original_name_offset == first.original_name_offset + }); + let iter_candidates: Vec<_> = if all_same { vec![first] } else { candidates }; + + let mut frames = Vec::new(); + for member in iter_candidates { + let mut mapped = map_member_without_lines(cache, frame, member, outer_source_file)?; + if suppress_line { + mapped.line = 0; + } else if is_implicit_no_range(member) && member.original_startline > 0 { + mapped.line = member.original_startline as usize; + } + frames.push(mapped); + } + + if frames.is_empty() { + None + } else { + Some(frames) + } +} + +enum NoLineSelection<'a> { + ExplicitBase(Vec<&'a raw::Member>, bool), + ImplicitNoRange(Vec<&'a raw::Member>, bool), + All(Vec<&'a raw::Member>, bool), +} + +fn select_no_line_selection<'a>(members: &'a [raw::Member]) -> NoLineSelection<'a> { + let explicit_base: Vec<_> = members.iter().filter(|m| is_explicit_base(m)).collect(); + if !explicit_base.is_empty() { + let has_span = explicit_base + .iter() + .any(|m| m.original_endline != u32::MAX && m.original_endline > m.original_startline); + return NoLineSelection::ExplicitBase(explicit_base, has_span); + } + + let implicit_no_range: Vec<_> = members.iter().filter(|m| is_implicit_no_range(m)).collect(); + if !implicit_no_range.is_empty() { + let suppress_line = implicit_no_range.len() > 1; + let mut implicit_no_range = implicit_no_range; + implicit_no_range.sort_by_key(|m| { + if m.original_startline > 0 { + m.original_startline + } else { + u32::MAX + } + }); + return NoLineSelection::ImplicitNoRange(implicit_no_range, suppress_line); + } + + NoLineSelection::All(members.iter().collect(), true) +} + +fn collect_base_frames_with_line<'a>( + cache: &ProguardCache<'a>, + frame: &StackFrame<'a>, + members: &[raw::Member], + outer_source_file: Option<&str>, +) -> Option>> { + let mut frames = Vec::new(); + for member in members { + let is_base = is_explicit_base(member); + let is_implicit = is_implicit_no_range(member); + if !(is_base || is_implicit) { + continue; + } + if member.original_endline != u32::MAX + && member.original_endline > member.original_startline + && member.original_startline > 0 + { + for line in member.original_startline..=member.original_endline { + let mut mapped = map_member_without_lines(cache, frame, member, outer_source_file)?; + mapped.line = line as usize; + frames.push(mapped); + } + continue; + } + + let mut mapped = map_member_without_lines(cache, frame, member, outer_source_file)?; + if is_implicit && member.original_startline > 0 { + mapped.line = member.original_startline as usize; + } + frames.push(mapped); + } + + if frames.is_empty() { + None + } else { + Some(frames) + } +} + fn iterate_with_lines<'a>( cache: &ProguardCache<'a>, frame: &mut StackFrame<'a>, @@ -980,25 +1134,34 @@ fn iterate_with_lines<'a>( has_line_info: bool, ) -> Option> { for member in members { - // If this method has line mappings, skip base (no-line) entries when we have a concrete line. - if has_line_info && frame.line > 0 && member.endline == 0 { + let is_base = is_explicit_base(member); + let is_implicit = is_implicit_no_range(member); + // If this method has line mappings, skip base and implicit entries when we have a line. + if has_line_info && frame.line > 0 && (is_base || is_implicit) { continue; } - // If the mapping entry has no line range, preserve the input line number (if any). - if member.endline == 0 { - return map_member_without_lines(cache, frame, member, outer_source_file); + // If the mapping entry has no line range, remap without line filters. + if is_base || is_implicit { + let mut mapped = map_member_without_lines(cache, frame, member, outer_source_file)?; + if is_implicit && frame.line > 0 && member.original_startline > 0 { + mapped.line = member.original_startline as usize; + } + return Some(mapped); } // skip any members which do not match our frames line - if member.endline > 0 + if has_line_range(member) + && member.startline <= member.endline && (frame.line < member.startline as usize || frame.line > member.endline as usize) { continue; } // parents of inlined frames don't have an `endline`, and // the top inlined frame need to be correctly offset. - let line = if member.original_endline == u32::MAX - || member.original_endline == member.original_startline - { + let line = if member.original_endline == u32::MAX { + member.original_startline as usize + } else if member.original_endline < member.original_startline { + frame.line + } else if member.original_endline == member.original_startline { member.original_startline as usize } else { member.original_startline as usize + frame.line - member.startline as usize @@ -1039,47 +1202,6 @@ fn iterate_with_lines<'a>( None } -/// Selection strategy for line==0 frames. -/// -/// When line info is missing, we prefer base (no-line) mappings if they exist. -/// If all candidates resolve to the same original method, we treat it as -/// unambiguous and return a single mapping. Otherwise we iterate either over -/// base mappings (when present) or all mappings (when only line-mapped entries exist). -enum NoLineSelection<'a> { - Single(&'a raw::Member), - IterateBase, - IterateAll, -} - -fn select_no_line_members<'a>(members: &'a [raw::Member]) -> Option> { - // Prefer base entries (endline == 0) if present. - let mut base_members = members.iter().filter(|m| m.endline == 0); - if let Some(first_base) = base_members.next() { - let all_same = base_members.all(|m| { - m.original_class_offset == first_base.original_class_offset - && m.original_name_offset == first_base.original_name_offset - }); - - return Some(if all_same { - NoLineSelection::Single(first_base) - } else { - NoLineSelection::IterateBase - }); - } - - let first = members.first()?; - let unambiguous = members.iter().all(|m| { - m.original_class_offset == first.original_class_offset - && m.original_name_offset == first.original_name_offset - }); - - Some(if unambiguous { - NoLineSelection::Single(first) - } else { - NoLineSelection::IterateAll - }) -} - fn map_member_without_lines<'a>( cache: &ProguardCache<'a>, frame: &StackFrame<'a>, @@ -1139,21 +1261,21 @@ fn iterate_without_lines<'a>( }) } -// For no-line mappings, prefer the original line if the mapping has no minified range. +// For explicit 0:0 mappings, prefer the original line when available. fn resolve_no_line_output_line( frame_line: usize, original_startline: u32, startline: u32, endline: u32, ) -> usize { - if frame_line > 0 { + if startline == 0 && endline == 0 { + if original_startline > 0 && original_startline != u32::MAX { + original_startline as usize + } else { + 0 + } + } else if frame_line > 0 { frame_line - } else if startline == 0 - && endline == 0 - && original_startline > 0 - && original_startline != u32::MAX - { - original_startline as usize } else { 0 } diff --git a/src/mapper.rs b/src/mapper.rs index 775e616..66d8578 100644 --- a/src/mapper.rs +++ b/src/mapper.rs @@ -98,6 +98,20 @@ struct CollectedFrames<'s> { type MemberIter<'m> = std::slice::Iter<'m, MemberMapping<'m>>; +const NO_MINIFIED_RANGE: usize = usize::MAX; + +fn is_implicit_no_range(member: &MemberMapping<'_>) -> bool { + member.startline == NO_MINIFIED_RANGE && member.endline == NO_MINIFIED_RANGE +} + +fn is_explicit_base(member: &MemberMapping<'_>) -> bool { + member.startline == 0 && member.endline == 0 +} + +fn has_line_range(member: &MemberMapping<'_>) -> bool { + !is_implicit_no_range(member) && member.endline > 0 +} + /// An Iterator over remapped StackFrames. #[derive(Clone, Debug, Default)] pub struct RemappedFrameIter<'m> { @@ -175,18 +189,20 @@ fn map_member_with_lines<'a>( frame: &StackFrame<'a>, member: &MemberMapping<'a>, ) -> Option> { - if member.endline > 0 && (frame.line < member.startline || frame.line > member.endline) { + if has_line_range(member) + && member.startline <= member.endline + && (frame.line < member.startline || frame.line > member.endline) + { return None; } // parents of inlined frames don't have an `endline`, and // the top inlined frame need to be correctly offset. - let line = if member.original_endline.is_none() - || member.original_endline == Some(member.original_startline) - { - member.original_startline - } else { - member.original_startline + frame.line - member.startline + let line = match member.original_endline { + None => member.original_startline, + Some(end) if end < member.original_startline => frame.line, + Some(end) if end == member.original_startline => member.original_startline, + Some(_) => member.original_startline + frame.line - member.startline, }; let class = member.original_class.unwrap_or(frame.class); @@ -220,11 +236,15 @@ fn map_member_without_lines<'a>( let class = member.original_class.unwrap_or(frame.class); // Synthesize from class name (input filename is not reliable) let file = synthesize_source_file(class, member.outer_source_file).map(Cow::Owned); - let line = if frame.line > 0 { + let line = if is_explicit_base(member) { + // Explicit 0:0 mappings suppress minified lines; prefer original if available. + if member.original_startline > 0 { + member.original_startline + } else { + 0 + } + } else if frame.line > 0 { frame.line - } else if member.startline == 0 && member.endline == 0 && member.original_startline > 0 { - // For base (no-line) mappings, use the original line when available. - member.original_startline } else { 0 }; @@ -232,8 +252,8 @@ fn map_member_without_lines<'a>( class, method: member.original, file, - // Preserve input line if present (e.g. "Unknown Source:7") when the mapping itself - // has no line information. This matches R8 retrace behavior. + // Preserve input line when available, unless this is an explicit 0:0 mapping with + // an original line (which should take precedence in R8 retrace behavior). line, parameters: frame.parameters, method_synthesized: member.is_synthesized, @@ -252,42 +272,105 @@ fn remap_class_only<'a>(frame: &StackFrame<'a>, reference_file: Option<&str>) -> } } -/// Selection strategy for line==0 frames. -/// -/// When line info is missing, we prefer base (no-line) mappings if they exist. -/// If all candidates resolve to the same original method, we treat it as -/// unambiguous and return a single mapping. Otherwise we iterate either over -/// base mappings (when present) or all mappings (when only line-mapped entries exist). enum NoLineSelection<'a> { - Single(&'a MemberMapping<'a>), - IterateAll, - IterateBase, + ExplicitBase(Vec<&'a MemberMapping<'a>>, bool), + ImplicitNoRange(Vec<&'a MemberMapping<'a>>, bool), + All(Vec<&'a MemberMapping<'a>>, bool), } -fn select_no_line_members<'a>( - mapping_entries: &'a [MemberMapping<'a>], - has_line_info: bool, -) -> Option> { - let mut base_members = mapping_entries.iter().filter(|m| m.endline == 0); - if has_line_info { - if let Some(first_base) = base_members.next() { - let all_same = base_members.all(|m| m.original == first_base.original); - return Some(if all_same { - NoLineSelection::Single(first_base) +fn select_no_line_selection<'a>(mapping_entries: &'a [MemberMapping<'a>]) -> NoLineSelection<'a> { + let explicit_base: Vec<_> = mapping_entries + .iter() + .filter(|m| is_explicit_base(m)) + .collect(); + if !explicit_base.is_empty() { + let has_span = explicit_base.iter().any(|m| { + m.original_endline + .is_some_and(|end| end > m.original_startline) + }); + return NoLineSelection::ExplicitBase(explicit_base, has_span); + } + + let implicit_no_range: Vec<_> = mapping_entries + .iter() + .filter(|m| is_implicit_no_range(m)) + .collect(); + if !implicit_no_range.is_empty() { + let suppress_line = implicit_no_range.len() > 1; + let mut implicit_no_range = implicit_no_range; + implicit_no_range.sort_by_key(|m| { + if m.original_startline > 0 { + m.original_startline } else { - NoLineSelection::IterateBase - }); + usize::MAX + } + }); + return NoLineSelection::ImplicitNoRange(implicit_no_range, suppress_line); + } + + NoLineSelection::All(mapping_entries.iter().collect(), true) +} + +fn collect_no_line_frames<'a>( + frame: &StackFrame<'a>, + mapping_entries: &'a [MemberMapping<'a>], + collected: &mut CollectedFrames<'a>, +) { + let selection = select_no_line_selection(mapping_entries); + let (candidates, suppress_line) = match selection { + NoLineSelection::ExplicitBase(candidates, suppress_line) => (candidates, suppress_line), + NoLineSelection::ImplicitNoRange(candidates, suppress_line) => (candidates, suppress_line), + NoLineSelection::All(candidates, suppress_line) => (candidates, suppress_line), + }; + + let Some(first) = candidates.first().copied() else { + return; + }; + let all_same = candidates.iter().all(|m| m.original == first.original); + let iter_candidates: Vec<_> = if all_same { vec![first] } else { candidates }; + + for member in iter_candidates { + let mut mapped = map_member_without_lines(frame, member); + if suppress_line { + mapped.line = 0; + } else if is_implicit_no_range(member) && member.original_startline > 0 { + mapped.line = member.original_startline; } + collected.frames.push(mapped); + collected.rewrite_rules.extend(member.rewrite_rules.iter()); } +} - let first = mapping_entries.first()?; - let unambiguous = mapping_entries.iter().all(|m| m.original == first.original); +fn collect_base_frames_with_line<'a>( + frame: &StackFrame<'a>, + mapping_entries: &'a [MemberMapping<'a>], + collected: &mut CollectedFrames<'a>, +) { + for member in mapping_entries { + let is_base = is_explicit_base(member); + let is_implicit = is_implicit_no_range(member); + if !(is_base || is_implicit) { + continue; + } + if let Some(original_endline) = member.original_endline { + if original_endline > member.original_startline && member.original_startline > 0 { + for line in member.original_startline..=original_endline { + let mut mapped = map_member_without_lines(frame, member); + mapped.line = line; + collected.frames.push(mapped); + collected.rewrite_rules.extend(member.rewrite_rules.iter()); + } + continue; + } + } - Some(if unambiguous { - NoLineSelection::Single(first) - } else { - NoLineSelection::IterateAll - }) + let mut mapped = map_member_without_lines(frame, member); + if is_implicit && member.original_startline > 0 { + mapped.line = member.original_startline; + } + collected.frames.push(mapped); + collected.rewrite_rules.extend(member.rewrite_rules.iter()); + } } fn apply_rewrite_rules<'s>(collected: &mut CollectedFrames<'s>, thrown_descriptor: Option<&str>) { @@ -328,13 +411,19 @@ fn iterate_with_lines<'a>( has_line_info: bool, ) -> Option> { for member in members { - // If this method has line mappings, skip base (no-line) entries when we have a concrete line. - if has_line_info && frame.line > 0 && member.endline == 0 { + let is_base = is_explicit_base(member); + let is_implicit = is_implicit_no_range(member); + // If this method has line mappings, skip base and implicit entries when we have a line. + if has_line_info && frame.line > 0 && (is_base || is_implicit) { continue; } - // If the mapping entry has no line range, preserve the input line number (if any). - if member.endline == 0 { - return Some(map_member_without_lines(frame, member)); + // If the mapping entry has no line range, remap without line filters. + if is_base || is_implicit { + let mut mapped = map_member_without_lines(frame, member); + if is_implicit && frame.line > 0 && member.original_startline > 0 { + mapped.line = member.original_startline; + } + return Some(mapped); } if let Some(mapped) = map_member_with_lines(frame, member) { return Some(mapped); @@ -612,54 +701,48 @@ impl<'s> ProguardMapper<'s> { }; if frame.parameters.is_none() { - let has_line_info = mapping_entries.iter().any(|m| m.endline > 0); + let has_line_info = mapping_entries.iter().any(|m| has_line_range(m)); + if frame.line > 0 + && frame.file().is_some_and(|file| file.is_empty()) + && mapping_entries.len() == 1 + && is_implicit_no_range(&mapping_entries[0]) + { + // Treat empty file names like "(:2)" as no-line info when there's only a single + // implicit no-range mapping (R8 falls back to the original line in that case). + frame.line = 0; + } // If the stacktrace has no line number, treat it as unknown and remap without - // applying line filters. If there are base (no-line) mappings present, prefer those. + // applying line filters. If there are explicit 0:0 mappings, prefer those. if frame.line == 0 { - let selection = select_no_line_members(mapping_entries, has_line_info); - match selection { - Some(NoLineSelection::Single(member)) => { - collected - .frames - .push(map_member_without_lines(&frame, member)); - collected.rewrite_rules.extend(member.rewrite_rules.iter()); - } - Some(NoLineSelection::IterateAll) => { - for member in mapping_entries { - collected - .frames - .push(map_member_without_lines(&frame, member)); - collected.rewrite_rules.extend(member.rewrite_rules.iter()); - } - } - Some(NoLineSelection::IterateBase) => { - for member in mapping_entries.iter().filter(|m| m.endline == 0) { - collected - .frames - .push(map_member_without_lines(&frame, member)); - collected.rewrite_rules.extend(member.rewrite_rules.iter()); - } - } - None => return collected, - } + collect_no_line_frames(&frame, mapping_entries, &mut collected); return collected; } for member in mapping_entries { - if has_line_info && member.endline == 0 { + let is_base = is_explicit_base(member); + let is_implicit = is_implicit_no_range(member); + if has_line_info && (is_base || is_implicit) { continue; } - if member.endline == 0 { - collected - .frames - .push(map_member_without_lines(&frame, member)); - collected.rewrite_rules.extend(member.rewrite_rules.iter()); + + if is_base || is_implicit { + collect_base_frames_with_line( + &frame, + std::slice::from_ref(member), + &mut collected, + ); } else if let Some(mapped) = map_member_with_lines(&frame, member) { collected.frames.push(mapped); collected.rewrite_rules.extend(member.rewrite_rules.iter()); } } + + if collected.frames.is_empty() { + collected + .frames + .push(remap_class_only(&frame, frame.file())); + } } else { for member in mapping_entries { let mapped = map_member_without_lines(&frame, member); @@ -724,7 +807,7 @@ impl<'s> ProguardMapper<'s> { members.all_mappings.iter() }; - let has_line_info = members.all_mappings.iter().any(|m| m.endline > 0); + let has_line_info = members.all_mappings.iter().any(|m| has_line_range(m)); RemappedFrameIter::members(frame, mappings, has_line_info) } diff --git a/src/mapping.rs b/src/mapping.rs index f9668d6..3fdc28e 100644 --- a/src/mapping.rs +++ b/src/mapping.rs @@ -643,10 +643,19 @@ fn parse_proguard_field_or_method( original_endline, }), // Preserve original line info even when no minified range is present. - // This enables this crate to use the original line for no-line mappings. + // Use a sentinel to distinguish from an explicit 0:0 minified range, but keep + // an explicit 0:0 when the original line is also 0. (None, None, Some(original_startline)) => Some(LineMapping { - startline: 0, - endline: 0, + startline: if original_startline == 0 { + 0 + } else { + usize::MAX + }, + endline: if original_startline == 0 { + 0 + } else { + usize::MAX + }, original_startline: Some(original_startline), original_endline, }), diff --git a/tests/r8-line-number-handling.NOTES.md b/tests/r8-line-number-handling.NOTES.md deleted file mode 100644 index a475f7e..0000000 --- a/tests/r8-line-number-handling.NOTES.md +++ /dev/null @@ -1,122 +0,0 @@ -# R8 Retrace: Line Number Handling — Current Failures & Needed Fixes - -This note accompanies `tests/r8-line-number-handling.rs`. - -Status when this note was written: - -- **10 tests total** -- **2 passing**: `test_obfuscated_range_to_single_line_stacktrace`, `test_preamble_line_number_stacktrace` -- **8 failing**: listed below - -Like other ported suites, these tests: - -- **Omit upstream `` markers** and list alternatives as duplicate frames. -- **Normalize expected indentation** to this crate’s output (`" at ..."`). -- **Use `:0`** for “no line info” since this crate represents missing line numbers as `0`. - -Below is a **one-by-one** explanation of the remaining failures and what behavior in the crate likely needs fixing. - -## 1) `test_no_obfuscation_range_mapping_with_stacktrace` - -- **Expected**: - - `foo.a(…:0)` retraces to `foo(long):1:1` → `Main.foo(Main.java:1)` - - `foo.b(…:2)` retraces to `bar(int):3` → `Main.bar(Main.java:3)` - - For `0:0` and `0` mappings, upstream expects “use original line info semantics” (see upstream fixture comment). -- **Actual**: - - `Main.foo(Main.java:0)` (lost the `:1`) - - `Main.bar(Main.java:2)` (seems to preserve the **minified** line `2` rather than mapping to `3`) - - `baz` and `main` keep minified lines `8`/`7` rather than dropping/normalizing. -- **What needs fixing**: - - The crate’s “base mapping” (`0`, `0:0`) line-number semantics don’t match R8: - - Some cases should map to the **original** line (e.g. `:1` for `foo`) - - Some cases should prefer the **method’s declared original line** even when minified line is present (e.g. `bar(int):3`) - - Some `0:0` entries should use the **stacktrace line** (R8’s special-case behavior). - - The logic likely lives in member selection / line translation in `src/mapper.rs` / cache iteration paths. - -## 2) `test_multiple_lines_no_line_number_stacktrace` - -- **Expected** (no line in stacktrace): - - Choose the `0:0` entries (base mappings) and emit their original lines: - - `method1(Main.java:42)` and `main(Main.java:28)` -- **Actual**: - - Emits both with `:0`. -- **What needs fixing**: - - When the stacktrace has “no line” (`Unknown Source`), and the mapping provides `0:0:…:origLine:origLine` (or explicit original line metadata), we should be able to emit those original lines instead of forcing `0`. - - Today we are collapsing “unknown line” to numeric `0` too early and then losing the mapping’s original line information. - -## 3) `test_single_line_no_line_number_stacktrace` - -- **Expected**: - - Base mappings (`0:0`) for `a` and `b` should expand into multiple original methods (`method1` + `main`, etc.) with specific original lines where available. - - For `c`, upstream emits two alternatives (`main3` and `method3`) and preserves their source context. - - `main4` should preserve its declared original line `153`. -- **Actual**: - - Everything ends up as `:0` (e.g. `method1(Main.java:0)`, `main4(Main.java:0)`). -- **What needs fixing**: - - Same core issue as (3), but more visible: - - Preserve/emit mapping-derived original lines for `0:0` entries. - - Don’t convert “unknown” into `0` in a way that prevents later line reconstruction. - -## 4) `test_no_obfuscated_line_number_with_override` - -- **Expected**: - - `main(Unknown Source)` still maps to `Main.main(Main.java:3)` because the mapping has a single `main(...):3`. - - `overload(Unknown Source)` yields both overloads but without line suffixes in the non-verbose output. - - `mainPC(:3)` should map to `Main.java:42` (mapping line is `42`). -- **Actual**: - - Most frames show `:0`, and `mainPC` shows `:3` (minified line preserved) instead of `:42`. -- **What needs fixing**: - - When obfuscated line numbers are missing (`Unknown Source`) but mapping provides a concrete original line, we should emit it (e.g. `main:3`). - - For `mainPC(:3)`, we’re not translating minified `3` to original `42` even though the mapping is unambiguous. - - This points to incorrect or missing “no obfuscated line number override” behavior in remapping. - -## 5) `test_different_line_number_span_stacktrace` - -- **Expected**: - - The mapping says `method1(...):42:44 -> a` and the stacktrace is `a.a(…:1)`. - - Upstream expands this to **three** possible original lines `42`, `43`, `44` (span). -- **Actual**: - - Only one frame, and it uses the minified line `1` as the output line. -- **What needs fixing**: - - For mappings that define a span of original lines for a single minified line (or ambiguous mapping within a span), we need to expand into the full set of candidate original lines rather than carrying through the minified line. - - This is core “line span expansion” logic (member lookup + line translation). - -## 6) `test_outside_line_range_stacktrace` - -- **Expected**: - - `a.a(:2)` and `a.a(Unknown Source)` both map to `some.other.Class.method1(Class.java:42)` - - `b.a(:27)` maps to `some.Class.a(Class.java:27)` (outside any range → fall back to the “unmapped member name” for that class, per fixture) - - `b.a(Unknown Source)` maps to `some.Class.method2(Class.java)` (no line) -- **Actual**: - - `some.other.Class.method1(Class.java:2)` and `...:0` (line propagation wrong) - - One line remains unparsed and unchanged: `at b.a(:27)` (it is emitted verbatim when not remapped) - - Last frame becomes `method2(Class.java:0)` instead of `method2(Class.java)` -- **What needs fixing**: - - **Parsing / fallback**: the `(:27)` location should be parsed and then remapped (or at least “best-effort” remapped), but currently it falls back to printing the original frame line. - - **Outside-range semantics**: when the minified line is outside any mapped range, decide how to choose: - - either fall back to a “best effort” member name remap, - - or keep obfuscated, but the expected behavior is a best-effort remap. - - **No-line formatting**: `Class.java` vs `Class.java:0` (same as (1)). - -## 7) `test_invalid_minified_range_stacktrace` - -- **Expected**: - - Even though the mapping has an invalid minified range (`5:3`), upstream still retraces the method and produces `Main.java:3`. -- **Actual**: - - The input line is emitted unchanged (not retraced). -- **What needs fixing**: - - The mapping parser / remapper currently rejects or ignores invalid minified ranges entirely. - - Upstream seems to treat this as recoverable and still uses the information to retrace. - - Implement more tolerant handling of invalid minified ranges (or normalize them) so retrace still occurs. - -## 8) `test_invalid_original_range_stacktrace` - -- **Expected**: - - For an invalid original range (`:5:2`), upstream still retraces and emits `Main.java:3`. -- **Actual**: - - Emits `Main.java:6` (wrong translation). -- **What needs fixing**: - - The translation logic from minified line → original line is not handling inverted original ranges correctly. - - Needs clamping / normalization rules consistent with R8 (e.g. treat as single line, or swap, or ignore original span and use minified). - - diff --git a/tests/r8-line-number-handling.rs b/tests/r8-line-number-handling.rs index 190da5a..bc2c0dd 100644 --- a/tests/r8-line-number-handling.rs +++ b/tests/r8-line-number-handling.rs @@ -51,8 +51,8 @@ fn test_no_obfuscation_range_mapping_with_stacktrace() { let expected = r#"Exception in thread "main" java.lang.NullPointerException at com.android.tools.r8.naming.retrace.Main.foo(Main.java:1) at com.android.tools.r8.naming.retrace.Main.bar(Main.java:3) - at com.android.tools.r8.naming.retrace.Main.baz(Main.java) - at com.android.tools.r8.naming.retrace.Main.main(Main.java) + at com.android.tools.r8.naming.retrace.Main.baz(Main.java:0) + at com.android.tools.r8.naming.retrace.Main.main(Main.java:0) "#; assert_remap_stacktrace( From 764b7f426cb842c751a2407d9b1a7241676aa040 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 29 Jan 2026 23:08:57 +0100 Subject: [PATCH 18/20] fix(r8-tests): Fix no-line original line preservation for source file edge cases (#79) - Split source file names by the last `:` occurrence - Additionally fixes no-line original line preservation - Also removes `tests/r8-source-file-edge-cases.NOTES.md` - Removes two tests that we agreed do not need fixing --- src/cache/mod.rs | 72 ++++++++-------------- src/lib.rs | 6 +- src/mapper.rs | 56 +++++------------ src/mapping.rs | 12 +++- src/stacktrace.rs | 4 +- src/utils.rs | 62 +++++++++++++++++++ tests/r8-source-file-edge-cases.NOTES.md | 60 ------------------- tests/r8-source-file-edge-cases.rs | 76 +----------------------- 8 files changed, 121 insertions(+), 227 deletions(-) create mode 100644 src/utils.rs delete mode 100644 tests/r8-source-file-edge-cases.NOTES.md diff --git a/src/cache/mod.rs b/src/cache/mod.rs index e6ec744..c2c1ac9 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -80,6 +80,10 @@ use thiserror::Error; use crate::builder::{RewriteAction, RewriteCondition, RewriteRule}; use crate::mapper::{format_cause, format_frames, format_throwable}; +use crate::utils::{ + class_name_to_descriptor, extract_class_name, resolve_no_line_output_line, + synthesize_source_file, +}; use crate::{java, stacktrace, DeobfuscatedSignature, StackFrame, StackTrace, Throwable}; pub use raw::{ProguardCache, PRGCACHE_VERSION}; @@ -1092,12 +1096,21 @@ fn map_member_without_lines<'a>( let method = cache.read_string(member.original_name_offset).ok()?; let file = synthesize_source_file(class, outer_source_file).map(Cow::Owned); + let original_startline = match member.original_startline { + 0 | u32::MAX => None, + value => Some(value as usize), + }; + Some(StackFrame { class, method, file, - // Preserve input line if present when the mapping has no line info. - line: frame.line, + line: resolve_no_line_output_line( + frame.line, + original_startline, + member.startline as usize, + member.endline as usize, + ), parameters: frame.parameters, method_synthesized: member.is_synthesized(), }) @@ -1120,59 +1133,26 @@ fn iterate_without_lines<'a>( // Synthesize from class name (input filename is not reliable) let file = synthesize_source_file(class, outer_source_file).map(Cow::Owned); + let original_startline = match member.original_startline { + 0 | u32::MAX => None, + value => Some(value as usize), + }; + Some(StackFrame { class, method, file, - // Preserve input line if present when the mapping has no line info. - line: frame.line, + line: resolve_no_line_output_line( + frame.line, + original_startline, + member.startline as usize, + member.endline as usize, + ), parameters: frame.parameters, method_synthesized: member.is_synthesized(), }) } -fn extract_class_name(full_path: &str) -> Option<&str> { - let after_last_period = full_path.split('.').next_back()?; - // If the class is an inner class, we need to extract the outer class name - after_last_period.split('$').next() -} - -/// 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" -fn synthesize_source_file(class_name: &str, reference_file: Option<&str>) -> Option { - let base = extract_class_name(class_name)?; - - // 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)); - } - - // If we have a reference file, derive extension from it - if let Some(ext) = reference_file.and_then(|f| f.rfind('.').map(|pos| &f[pos..])) { - return Some(format!("{}{}", base, ext)); - } - - Some(format!("{}.java", base)) -} - -/// Converts a Java class name to its JVM descriptor format. -/// -/// For example, `java.lang.NullPointerException` becomes `Ljava/lang/NullPointerException;`. -pub fn class_name_to_descriptor(class: &str) -> String { - let mut descriptor = String::with_capacity(class.len() + 2); - descriptor.push('L'); - descriptor.push_str(&class.replace('.', "/")); - descriptor.push(';'); - descriptor -} - /// Computes the number of frames to skip based on rewrite rules. /// Returns the total skip count from all matching RemoveInnerFrames actions. fn compute_skip_count(rewrite_rules: &[RewriteRule<'_>], thrown_descriptor: Option<&str>) -> usize { diff --git a/src/lib.rs b/src/lib.rs index a9a270e..18c04e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,13 +42,13 @@ mod java; mod mapper; mod mapping; mod stacktrace; +mod utils; -pub use cache::{ - class_name_to_descriptor, CacheError, CacheErrorKind, ProguardCache, PRGCACHE_VERSION, -}; +pub use cache::{CacheError, CacheErrorKind, ProguardCache, PRGCACHE_VERSION}; pub use mapper::{DeobfuscatedSignature, ProguardMapper, RemappedFrameIter}; pub use mapping::{ LineMapping, MappingSummary, ParseError, ParseErrorKind, ProguardMapping, ProguardRecord, ProguardRecordIter, }; pub use stacktrace::{StackFrame, StackTrace, Throwable}; +pub use utils::class_name_to_descriptor; diff --git a/src/mapper.rs b/src/mapper.rs index 827a0d5..4e419ed 100644 --- a/src/mapper.rs +++ b/src/mapper.rs @@ -10,6 +10,10 @@ use crate::builder::{ use crate::java; use crate::mapping::ProguardMapping; use crate::stacktrace::{self, StackFrame, StackTrace, Throwable}; +use crate::utils::{ + class_name_to_descriptor, extract_class_name, resolve_no_line_output_line, + synthesize_source_file, +}; /// A deobfuscated method signature. pub struct DeobfuscatedSignature { @@ -132,45 +136,6 @@ impl<'m> Iterator for RemappedFrameIter<'m> { } } -fn extract_class_name(full_path: &str) -> Option<&str> { - let after_last_period = full_path.split('.').next_back()?; - // If the class is an inner class, we need to extract the outer class name - after_last_period.split('$').next() -} - -fn class_name_to_descriptor(class: &str) -> String { - let mut descriptor = String::with_capacity(class.len() + 2); - descriptor.push('L'); - descriptor.push_str(&class.replace('.', "/")); - descriptor.push(';'); - descriptor -} - -/// Synthesizes a full source file name from a class name and a reference source file. -/// 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" -fn synthesize_source_file(class_name: &str, reference_file: Option<&str>) -> Option { - let base = extract_class_name(class_name)?; - - // 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)); - } - - // If we have a reference file, derive extension from it - if let Some(ext) = reference_file.and_then(|f| f.rfind('.').map(|pos| &f[pos..])) { - return Some(format!("{}{}", base, ext)); - } - - Some(format!("{}.java", base)) -} - fn map_member_with_lines<'a>( frame: &StackFrame<'a>, member: &MemberMapping<'a>, @@ -220,13 +185,24 @@ fn map_member_without_lines<'a>( let class = member.original_class.unwrap_or(frame.class); // Synthesize from class name (input filename is not reliable) let file = synthesize_source_file(class, member.outer_source_file).map(Cow::Owned); + let original_startline = if member.original_startline > 0 { + Some(member.original_startline) + } else { + None + }; + let line = resolve_no_line_output_line( + frame.line, + original_startline, + member.startline, + member.endline, + ); StackFrame { class, method: member.original, file, // Preserve input line if present (e.g. "Unknown Source:7") when the mapping itself // has no line information. This matches R8 retrace behavior. - line: frame.line, + line, parameters: frame.parameters, method_synthesized: member.is_synthesized, } diff --git a/src/mapping.rs b/src/mapping.rs index e433117..f9668d6 100644 --- a/src/mapping.rs +++ b/src/mapping.rs @@ -635,13 +635,21 @@ fn parse_proguard_field_or_method( })?; let original_class = split_class.next(); - let line_mapping = match (startline, endline) { - (Some(startline), Some(endline)) => Some(LineMapping { + let line_mapping = match (startline, endline, original_startline) { + (Some(startline), Some(endline), _) => Some(LineMapping { startline, endline, original_startline, original_endline, }), + // Preserve original line info even when no minified range is present. + // This enables this crate to use the original line for no-line mappings. + (None, None, Some(original_startline)) => Some(LineMapping { + startline: 0, + endline: 0, + original_startline: Some(original_startline), + original_endline, + }), _ => None, }; diff --git a/src/stacktrace.rs b/src/stacktrace.rs index 875f4ca..9e56806 100644 --- a/src/stacktrace.rs +++ b/src/stacktrace.rs @@ -286,8 +286,8 @@ pub(crate) fn parse_frame(line: &str) -> Option> { let (method_split, file_split) = line[3..line.len() - 1].split_once('(')?; let (class, method) = method_split.rsplit_once('.')?; - let (file, line) = match file_split.split_once(':') { - Some((file, line)) => (file, line.parse().ok()?), + let (file, line) = match file_split.rsplit_once(':') { + Some((file, line)) => (file, line.parse().unwrap_or(0)), None => (file_split, 0), }; diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..fa0089c --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,62 @@ +//! Internal helpers shared across modules. + +/// For no-line mappings, prefer the original line if the mapping has no minified range. +pub(crate) fn resolve_no_line_output_line( + frame_line: usize, + original_startline: Option, + startline: usize, + endline: usize, +) -> usize { + if frame_line > 0 { + frame_line + } else if startline == 0 && endline == 0 { + original_startline.unwrap_or(0) + } else { + 0 + } +} + +pub(crate) fn extract_class_name(full_path: &str) -> Option<&str> { + let after_last_period = full_path.split('.').next_back()?; + // If the class is an inner class, we need to extract the outer class name + after_last_period.split('$').next() +} + +/// 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" +pub(crate) fn synthesize_source_file( + class_name: &str, + reference_file: Option<&str>, +) -> Option { + let base = extract_class_name(class_name)?; + + // 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)); + } + + // If we have a reference file, derive extension from it + if let Some(ext) = reference_file.and_then(|f| f.rfind('.').map(|pos| &f[pos..])) { + return Some(format!("{}{}", base, ext)); + } + + Some(format!("{}.java", base)) +} + +/// Converts a Java class name to its JVM descriptor format. +/// +/// For example, `java.lang.NullPointerException` becomes `Ljava/lang/NullPointerException;`. +pub fn class_name_to_descriptor(class: &str) -> String { + let mut descriptor = String::with_capacity(class.len() + 2); + descriptor.push('L'); + descriptor.push_str(&class.replace('.', "/")); + descriptor.push(';'); + descriptor +} diff --git a/tests/r8-source-file-edge-cases.NOTES.md b/tests/r8-source-file-edge-cases.NOTES.md deleted file mode 100644 index 73a73f6..0000000 --- a/tests/r8-source-file-edge-cases.NOTES.md +++ /dev/null @@ -1,60 +0,0 @@ -# R8 Retrace: Source File Edge Cases — Remaining Fixes - -After normalizing fixture indentation (member mappings use 4 spaces), `tests/r8-source-file-edge-cases.rs` has **4 passing** and **3 failing** tests. - -This note documents, **one-by-one**, what still needs fixing in the crate (not in the fixtures) to make the remaining tests pass. - -## 1) `test_colon_in_file_name_stacktrace` - -- **Symptom**: Frames are emitted unchanged and do not get retraced: - - Input stays as `at a.s(:foo::bar:1)` / `at a.t(:foo::bar:)`. -- **Root cause**: `src/stacktrace.rs::parse_frame` splits `(:)` using the **first** `:`. - - For `(:foo::bar:1)`, the first split produces `file=""` and `line="foo::bar:1"`, so parsing the line number fails and the whole frame is rejected. -- **Fix needed**: - - In `parse_frame`, split `file:line` using the **last** colon (`rsplit_once(':')`) so file names can contain `:` (Windows paths and this fixture). - - Treat an empty or non-numeric suffix after the last colon as “no line info” (line `0`) instead of rejecting the frame. - -## 2) `test_file_name_extension_stacktrace` - -This failure is due to two independent gaps. - -### 2a) Weird location forms aren’t parsed/normalized consistently - -- **Symptom**: Output contains things like `Main.java:` and `R8.foo:0` instead of normalized `R8.java:0` for “no line” cases. -- **Root cause**: `parse_frame` only supports: - - `(:)`, or - - `()` (treated as line `0`), - and it currently rejects or mis-interprets inputs like: - - `(Native Method)`, `(Unknown Source)`, `(Unknown)`, `()` - - `(Main.java:)` (empty “line” part) - - `(Main.foo)` (no `:line`, but also not a normal source file extension) -- **Fix needed**: - - Make `parse_frame` permissive for these Java stacktrace forms and interpret them as a parsed frame with **line `0`** so remapping can then replace the file with the mapping’s source file (here: `R8.java`). - - Also apply the “split on last colon” rule from (1) so `file:line` parsing is robust. - -### 2b) `Suppressed:` throwables are not remapped - -- **Symptom**: The throwable in the `Suppressed:` line remains obfuscated: - - Actual: `Suppressed: a.b.c: You have to write the program first` - - Expected: `Suppressed: foo.bar.baz: You have to write the program first` -- **Root cause**: `src/mapper.rs::remap_stacktrace` remaps: - - the first-line throwable, and - - `Caused by: ...`, - but it does **not** detect/handle `Suppressed: ...`. -- **Fix needed**: - - Add handling for the `Suppressed: ` prefix analogous to `Caused by: `: - - parse the throwable after the prefix, - - remap it, - - emit with the same prefix. - -## 3) `test_class_with_dash_stacktrace` - -- **Symptom**: An extra frame appears: - - Actual includes `Unused.staticMethod(Unused.java:0)` in addition to `I.staticMethod(I.java:66)`. -- **Root cause**: The mapping includes synthesized metadata (`com.android.tools.r8.synthesized`) and multiple plausible remapped frames, including synthesized “holder/bridge” frames. - - Today we emit all candidates rather than preferring non-synthesized frames. -- **Fix needed**: - - Propagate the synthesized marker into `StackFrame.method_synthesized` during mapping. - - When multiple candidate remapped frames exist for one obfuscated frame, **filter synthesized frames** if any non-synthesized frames exist (or apply an equivalent preference rule). - - diff --git a/tests/r8-source-file-edge-cases.rs b/tests/r8-source-file-edge-cases.rs index 7b94c0a..bfeee9f 100644 --- a/tests/r8-source-file-edge-cases.rs +++ b/tests/r8-source-file-edge-cases.rs @@ -39,8 +39,8 @@ fn test_colon_in_file_name_stacktrace() { at a.t(:foo::bar:) "#; - let expected = r#" at some.Class.strawberry(Class.kt:99) - at some.Class.passionFruit(Class.kt:121) + let expected = r#" at some.Class.strawberry(Class.kt:99) + at some.Class.passionFruit(Class.kt:121) "#; assert_remap_stacktrace(COLON_IN_FILE_NAME_MAPPING, input, expected); @@ -91,50 +91,6 @@ fn test_multiple_dots_in_file_name_stacktrace() { assert_remap_stacktrace(MULTIPLE_DOTS_IN_FILE_NAME_MAPPING, input, expected); } -// ============================================================================= -// FileNameExtensionStackTrace -// ============================================================================= - -const FILE_NAME_EXTENSION_MAPPING: &str = "\ -foo.bar.baz -> a.b.c: -R8 -> R8: -"; - -#[test] -fn test_file_name_extension_stacktrace() { - let input = r#"a.b.c: Problem when compiling program - at R8.main(App:800) - at R8.main(Native Method) - at R8.main(Main.java:) - at R8.main(Main.kt:1) - at R8.main(Main.foo) - at R8.main() - at R8.main(Unknown) - at R8.main(SourceFile) - at R8.main(SourceFile:1) -Suppressed: a.b.c: You have to write the program first - at R8.retrace(App:184) - ... 7 more -"#; - - let expected = r#"foo.bar.baz: Problem when compiling program - at R8.main(R8.java:800) - at R8.main(R8.java:0) - at R8.main(R8.java:0) - at R8.main(R8.kt:1) - at R8.main(R8.java:0) - at R8.main(R8.java:0) - at R8.main(R8.java:0) - at R8.main(R8.java:0) - at R8.main(R8.java:1) -Suppressed: foo.bar.baz: You have to write the program first - at R8.retrace(R8.java:184) - ... 7 more -"#; - - assert_remap_stacktrace(FILE_NAME_EXTENSION_MAPPING, input, expected); -} - // ============================================================================= // SourceFileNameSynthesizeStackTrace // ============================================================================= @@ -185,31 +141,3 @@ fn test_source_file_with_number_and_empty_stacktrace() { assert_remap_stacktrace(SOURCE_FILE_WITH_NUMBER_AND_EMPTY_MAPPING, input, expected); } - -// ============================================================================= -// ClassWithDashStackTrace -// ============================================================================= - -const CLASS_WITH_DASH_MAPPING: &str = "\ -# {\"id\":\"com.android.tools.r8.mapping\",\"version\":\"1.0\"} -Unused -> I$-CC: -# {\"id\":\"com.android.tools.r8.synthesized\"} - 66:66:void I.staticMethod() -> staticMethod - 66:66:void staticMethod():0 -> staticMethod - # {\"id\":\"com.android.tools.r8.synthesized\"} -"; - -#[test] -fn test_class_with_dash_stacktrace() { - let input = r#"java.lang.NullPointerException - at I$-CC.staticMethod(I.java:66) - at Main.main(Main.java:73) -"#; - - let expected = r#"java.lang.NullPointerException - at I.staticMethod(I.java:66) - at Main.main(Main.java:73) -"#; - - assert_remap_stacktrace(CLASS_WITH_DASH_MAPPING, input, expected); -} From a97412f7dbe3143897ad5ccb0bbcfa732a5d2190 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 29 Jan 2026 23:31:08 +0100 Subject: [PATCH 19/20] Drop r8-synthetic --- tests/r8-synthetic.NOTES.md | 31 -------- tests/r8-synthetic.rs | 152 ------------------------------------ 2 files changed, 183 deletions(-) delete mode 100644 tests/r8-synthetic.NOTES.md delete mode 100644 tests/r8-synthetic.rs diff --git a/tests/r8-synthetic.NOTES.md b/tests/r8-synthetic.NOTES.md deleted file mode 100644 index 0ca9ca7..0000000 --- a/tests/r8-synthetic.NOTES.md +++ /dev/null @@ -1,31 +0,0 @@ -# r8-synthetic.rs failures - -This doc summarizes the current failures from running `cargo test --test r8-synthetic`. - -## `test_synthetic_lambda_method_stacktrace` - -- **Failure**: An extra synthetic frame is emitted: - - `example.Foo$$ExternalSyntheticLambda0.run(Foo.java:0)` -- **Expected**: Only the “real” deobfuscated frames (`lambda$main$0`, `runIt`, `main`, `Main.main`). -- **Why**: - - The mapper currently includes synthesized lambda bridge frames (marked via `com.android.tools.r8.synthesized`) instead of filtering them out when a better “real” frame exists. - - Also, `Unknown Source` maps to `:0` for line numbers, so the synthetic frame shows `Foo.java:0`. - -## `test_synthetic_lambda_method_with_inlining_stacktrace` - -- **Failure**: Same as above — extra synthetic frame: - - `example.Foo$$ExternalSyntheticLambda0.run(Foo.java:0)` -- **Expected**: No synthetic lambda `run(...)` frame in the output. -- **Why**: - - Same root cause: missing synthesized-frame suppression when a non-synthesized alternative exists. - -## `test_moved_synthetized_info_stacktrace` - -- **Failure**: An extra synthesized frame is emitted: - - `com.android.tools.r8.BaseCommand$Builder.inlinee$synthetic(BaseCommand.java:0)` -- **Expected**: Only: - - `com.android.tools.r8.BaseCommand$Builder.inlinee(BaseCommand.java:206)` -- **Why**: - - The mapping has both “real” and `$synthetic` variants, with synthesized metadata attached to the `$synthetic` variant. - - The mapper currently emits both candidates rather than filtering out the synthesized one when the non-synthesized target exists. - diff --git a/tests/r8-synthetic.rs b/tests/r8-synthetic.rs deleted file mode 100644 index 97d0b68..0000000 --- a/tests/r8-synthetic.rs +++ /dev/null @@ -1,152 +0,0 @@ -//! Tests for R8 synthetic / lambda method retracing fixtures. -//! -//! These tests are based on the R8 retrace test suite from: -//! src/test/java/com/android/tools/r8/retrace/stacktraces/ - -use proguard::{ProguardCache, ProguardMapper, ProguardMapping}; - -// ============================================================================= -// SyntheticLambdaMethodStackTrace -// ============================================================================= - -const SYNTHETIC_LAMBDA_METHOD_MAPPING: &str = "\ -# {'id':'com.android.tools.r8.mapping','version':'1.0'} -example.Main -> example.Main: - 1:1:void main(java.lang.String[]):123 -> main -example.Foo -> a.a: - 5:5:void lambda$main$0():225 -> a - 3:3:void runIt():218 -> b - 2:2:void main():223 -> c -example.Foo$$ExternalSyntheticLambda0 -> a.b: - void run(example.Foo) -> a - # {'id':'com.android.tools.r8.synthesized'} -"; - -#[test] -fn test_synthetic_lambda_method_stacktrace() { - let input = "\ -Exception in thread \"main\" java.lang.NullPointerException - at a.a.a(a.java:5) - at a.b.a(Unknown Source) - at a.a.b(a.java:3) - at a.a.c(a.java:2) - at example.Main.main(Main.java:1) -"; - - // Note: this crate prints 4 spaces before each remapped frame. - // Also, when no line info is available, it will output :0. - let expected = "\ -Exception in thread \"main\" java.lang.NullPointerException - at example.Foo.lambda$main$0(Foo.java:225) - at example.Foo.runIt(Foo.java:218) - at example.Foo.main(Foo.java:223) - at example.Main.main(Main.java:123) -"; - - let mapper = ProguardMapper::from(SYNTHETIC_LAMBDA_METHOD_MAPPING); - let actual = mapper.remap_stacktrace(input).unwrap(); - assert_eq!(actual.trim(), expected.trim()); - - let mapping = ProguardMapping::new(SYNTHETIC_LAMBDA_METHOD_MAPPING.as_bytes()); - let mut buf = Vec::new(); - ProguardCache::write(&mapping, &mut buf).unwrap(); - let cache = ProguardCache::parse(&buf).unwrap(); - cache.test(); - - let actual = cache.remap_stacktrace(input).unwrap(); - assert_eq!(actual.trim(), expected.trim()); -} - -// ============================================================================= -// SyntheticLambdaMethodWithInliningStackTrace -// ============================================================================= - -const SYNTHETIC_LAMBDA_METHOD_WITH_INLINING_MAPPING: &str = "\ -# {'id':'com.android.tools.r8.mapping','version':'1.0'} -example.Main -> example.Main: - 1:1:void main(java.lang.String[]):123 -> main -example.Foo -> a.a: - 3:3:void runIt():218 -> b - 2:2:void main():223 -> c -example.Foo$$ExternalSyntheticLambda0 -> a.b: - 4:4:void example.Foo.lambda$main$0():225 -> a - 4:4:void run(example.Foo):0 -> a - # {'id':'com.android.tools.r8.synthesized'} -"; - -#[test] -fn test_synthetic_lambda_method_with_inlining_stacktrace() { - let input = "\ -Exception in thread \"main\" java.lang.NullPointerException - at a.b.a(Unknown Source:4) - at a.a.b(a.java:3) - at a.a.c(a.java:2) - at example.Main.main(Main.java:1) -"; - - let expected = "\ -Exception in thread \"main\" java.lang.NullPointerException - at example.Foo.lambda$main$0(Foo.java:225) - at example.Foo.runIt(Foo.java:218) - at example.Foo.main(Foo.java:223) - at example.Main.main(Main.java:123) -"; - - let mapper = ProguardMapper::from(SYNTHETIC_LAMBDA_METHOD_WITH_INLINING_MAPPING); - let actual = mapper.remap_stacktrace(input).unwrap(); - assert_eq!(actual.trim(), expected.trim()); - - let mapping = ProguardMapping::new(SYNTHETIC_LAMBDA_METHOD_WITH_INLINING_MAPPING.as_bytes()); - let mut buf = Vec::new(); - ProguardCache::write(&mapping, &mut buf).unwrap(); - let cache = ProguardCache::parse(&buf).unwrap(); - cache.test(); - - let actual = cache.remap_stacktrace(input).unwrap(); - assert_eq!(actual.trim(), expected.trim()); -} - -// ============================================================================= -// MovedSynthetizedInfoStackTraceTest -// ============================================================================= - -const MOVED_SYNTHETIZED_INFO_MAPPING: &str = "\ -# { id: 'com.android.tools.r8.mapping', version: '2.2' } -com.android.tools.r8.BaseCommand$Builder -> foo.bar: - 1:1:void inlinee(java.util.Collection):0:0 -> inlinee$synthetic - 1:1:void inlinee$synthetic(java.util.Collection):0:0 -> inlinee$synthetic - 2:2:void inlinee(java.util.Collection):206:206 -> inlinee$synthetic - 2:2:void inlinee$synthetic(java.util.Collection):0:0 -> inlinee$synthetic - # {\"id\":\"com.android.tools.r8.synthesized\"} - 4:4:void inlinee(java.util.Collection):208:208 -> inlinee$synthetic - 4:4:void inlinee$synthetic(java.util.Collection):0 -> inlinee$synthetic - 7:7:void error(origin.Origin,java.lang.Throwable):363:363 -> inlinee$synthetic - 7:7:void inlinee(java.util.Collection):210 -> inlinee$synthetic - 7:7:void inlinee$synthetic(java.util.Collection):0:0 -> inlinee$synthetic -"; - -#[test] -fn test_moved_synthetized_info_stacktrace() { - let input = "\ -java.lang.RuntimeException: foobar - at foo.bar.inlinee$synthetic(BaseCommand.java:2) -"; - - let expected = "\ -java.lang.RuntimeException: foobar - at com.android.tools.r8.BaseCommand$Builder.inlinee(BaseCommand.java:206) -"; - - let mapper = ProguardMapper::from(MOVED_SYNTHETIZED_INFO_MAPPING); - let actual = mapper.remap_stacktrace(input).unwrap(); - assert_eq!(actual.trim(), expected.trim()); - - let mapping = ProguardMapping::new(MOVED_SYNTHETIZED_INFO_MAPPING.as_bytes()); - let mut buf = Vec::new(); - ProguardCache::write(&mapping, &mut buf).unwrap(); - let cache = ProguardCache::parse(&buf).unwrap(); - cache.test(); - - let actual = cache.remap_stacktrace(input).unwrap(); - assert_eq!(actual.trim(), expected.trim()); -} From 6e5070bec602e5e3d2b09f35810f1deeb7d221ae Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Fri, 30 Jan 2026 13:33:24 +0100 Subject: [PATCH 20/20] remove the tests we don't need --- tests/r8-exception-handling.NOTES.md | 44 ---------- tests/r8-exception-handling.rs | 119 --------------------------- 2 files changed, 163 deletions(-) delete mode 100644 tests/r8-exception-handling.NOTES.md diff --git a/tests/r8-exception-handling.NOTES.md b/tests/r8-exception-handling.NOTES.md deleted file mode 100644 index 585df58..0000000 --- a/tests/r8-exception-handling.NOTES.md +++ /dev/null @@ -1,44 +0,0 @@ -# R8 Exception Handling fixtures: failures & required behavior changes - -Ported from upstream R8 retrace fixtures under: -- `src/test/java/com/android/tools/r8/retrace/stacktraces/` - -This doc lists **only the failing tests** and explains, one-by-one, what would need to change in `rust-proguard` to match upstream R8 retrace behavior. (We keep expectations as-is; no behavior fixes here.) - -## `test_suppressed_stacktrace` - -- **Upstream behavior**: Throwable lines prefixed with `Suppressed:` still have their exception class retraced (e.g. `Suppressed: a.b.c: ...` → `Suppressed: foo.bar.baz: ...`). -- **Current crate behavior**: Leaves the suppressed exception class as `a.b.c`. -- **Why it fails**: `stacktrace::parse_throwable` recognizes normal throwables and `Caused by:`, but the `Suppressed:` prefix requires special-case parsing/stripping and then re-emitting with the same prefix. -- **What needs fixing**: - - **Throwable parsing**: treat `Suppressed:` lines as throwables (similar to `Caused by:`) and remap their class name. - -## `test_circular_reference_stacktrace` - -- **Upstream behavior**: Retrace rewrites `[CIRCULAR REFERENCE: X]` tokens by remapping `X` as a class name (when it looks like an obfuscated class). -- **Current crate behavior**: Leaves the input unchanged. -- **Why it fails**: These lines are neither parsed as throwables nor stack frames, so they currently fall through to “print as-is”. -- **What needs fixing**: - - **Extra line kinds**: add a parser/rewriter for circular-reference marker lines that extracts the referenced class name and applies `remap_class`. - - **Robustness**: keep the upstream behavior of only rewriting valid markers and leaving invalid marker formats unchanged. - -## `test_exception_message_with_class_name_in_message` - -- **Upstream behavior**: Retrace can replace obfuscated class names appearing inside arbitrary log/exception message text (here it replaces `net::ERR_CONNECTION_CLOSED` → `foo.bar.baz::ERR_CONNECTION_CLOSED`). -- **Current crate behavior**: Does not rewrite inside plain text lines. -- **Why it fails**: `remap_stacktrace` currently only remaps: - - throwable headers (`X: message`, `Caused by: ...`, etc.) - - parsed stack frames (`at ...`) - Everything else is emitted unchanged. -- **What needs fixing**: - - **Text rewriting pass** (R8-like): implement optional “message rewriting” for known patterns where an obfuscated class appears in text (in this fixture: a token that looks like `::`). - - **Scoping**: upstream uses context; we likely need a conservative implementation to avoid over-replacing. - -## `test_unknown_source_stacktrace` - -- **Expected in test**: deterministic ordering for ambiguous alternatives: `bar` then `foo`, repeated for each frame. -- **Current crate behavior**: emits the same set of alternatives but in the opposite order (`foo` then `bar`). -- **Why it fails**: ambiguous member ordering is currently determined by internal iteration order (mapping parse order / sorting) which does not match upstream’s ordering rules for this fixture. -- **What needs fixing**: - - **Stable ordering rule** for ambiguous alternatives (e.g., preserve mapping file order, or sort by original method name/signature in a defined way matching R8). - diff --git a/tests/r8-exception-handling.rs b/tests/r8-exception-handling.rs index 98bfe8d..be8f40f 100644 --- a/tests/r8-exception-handling.rs +++ b/tests/r8-exception-handling.rs @@ -54,89 +54,6 @@ Caused by: foo.bar.baz: You have to write the program first assert_remap_stacktrace(OBFUSCATED_EXCEPTION_CLASS_MAPPING, input, expected); } -// ============================================================================= -// SuppressedStackTrace -// ============================================================================= - -const SUPPRESSED_STACKTRACE_MAPPING: &str = r#"foo.bar.baz -> a.b.c: -"#; - -#[test] -fn test_suppressed_stacktrace() { - let input = r#"a.b.c: Problem when compiling program - at r8.main(App:800) -Suppressed: a.b.c: You have to write the program first - at r8.retrace(App:184) - ... 7 more -"#; - - let expected = r#"foo.bar.baz: Problem when compiling program - at r8.main(App:800) -Suppressed: foo.bar.baz: You have to write the program first - at r8.retrace(App:184) - ... 7 more -"#; - - assert_remap_stacktrace(SUPPRESSED_STACKTRACE_MAPPING, input, expected); -} - -// ============================================================================= -// CircularReferenceStackTrace -// ============================================================================= - -const CIRCULAR_REFERENCE_STACKTRACE_MAPPING: &str = r#"foo.bar.Baz -> A.A: -foo.bar.Qux -> A.B: -"#; - -#[test] -fn test_circular_reference_stacktrace() { - let input = r#" [CIRCULAR REFERENCE: A.A] - [CIRCULAR REFERENCE: A.B] - [CIRCULAR REFERENCE: None.existing.class] - [CIRCULAR REFERENCE: A.A] - [CIRCU:AA] - [CIRCULAR REFERENCE: A.A - [CIRCULAR REFERENCE: ] - [CIRCULAR REFERENCE: None existing class] -"#; - - let expected = r#" [CIRCULAR REFERENCE: foo.bar.Baz] - [CIRCULAR REFERENCE: foo.bar.Qux] - [CIRCULAR REFERENCE: None.existing.class] - [CIRCULAR REFERENCE: foo.bar.Baz] - [CIRCU:AA] - [CIRCULAR REFERENCE: foo.bar.Baz - [CIRCULAR REFERENCE: ] - [CIRCULAR REFERENCE: None existing class] -"#; - - assert_remap_stacktrace(CIRCULAR_REFERENCE_STACKTRACE_MAPPING, input, expected); -} - -// ============================================================================= -// ExceptionMessageWithClassNameInMessage -// ============================================================================= - -const EXCEPTION_MESSAGE_WITH_CLASSNAME_IN_MESSAGE_MAPPING: &str = r#"foo.bar.baz -> net: -"#; - -#[test] -fn test_exception_message_with_class_name_in_message() { - let input = r#"10-26 19:26:24.749 10159 26250 26363 E Tycho.crl: Exception -10-26 19:26:24.749 10159 26250 26363 E Tycho.crl: java.util.concurrent.ExecutionException: ary: eu: Exception in CronetUrlRequest: net::ERR_CONNECTION_CLOSED, ErrorCode=5, InternalErrorCode=-100, Retryable=true -"#; - - let expected = r#"10-26 19:26:24.749 10159 26250 26363 E Tycho.crl: Exception -10-26 19:26:24.749 10159 26250 26363 E Tycho.crl: java.util.concurrent.ExecutionException: ary: eu: Exception in CronetUrlRequest: foo.bar.baz::ERR_CONNECTION_CLOSED, ErrorCode=5, InternalErrorCode=-100, Retryable=true -"#; - - assert_remap_stacktrace( - EXCEPTION_MESSAGE_WITH_CLASSNAME_IN_MESSAGE_MAPPING, - input, - expected, - ); -} - // ============================================================================= // RetraceAssertionErrorStackTrace // ============================================================================= @@ -179,39 +96,3 @@ fn test_retrace_assertion_error_stacktrace() { assert_remap_stacktrace(RETRACE_ASSERTION_ERROR_STACKTRACE_MAPPING, input, expected); } - -// ============================================================================= -// UnknownSourceStackTrace -// ============================================================================= - -const UNKNOWN_SOURCE_STACKTRACE_MAPPING: &str = r#"com.android.tools.r8.R8 -> a.a: - void foo(int) -> a - void bar(int, int) -> a -"#; - -#[test] -fn test_unknown_source_stacktrace() { - let input = r#"com.android.tools.r8.CompilationException: foo[parens](Source:3) - at a.a.a(Unknown Source) - at a.a.a(Unknown Source) - at com.android.tools.r8.R8.main(Unknown Source) -Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3) - at a.a.a(Unknown Source) - ... 42 more -"#; - - // This crate does not format `` groups; alternatives are emitted as duplicate frames. - let expected = r#"com.android.tools.r8.CompilationException: foo[parens](Source:3) - at com.android.tools.r8.R8.bar(R8.java:0) - at com.android.tools.r8.R8.foo(R8.java:0) - at com.android.tools.r8.R8.bar(R8.java:0) - at com.android.tools.r8.R8.foo(R8.java:0) - at com.android.tools.r8.R8.main(Unknown Source) -Caused by: com.android.tools.r8.CompilationException: foo[parens](Source:3) - at com.android.tools.r8.R8.bar(R8.java:0) - at com.android.tools.r8.R8.foo(R8.java:0) - ... 42 more -"#; - - assert_remap_stacktrace(UNKNOWN_SOURCE_STACKTRACE_MAPPING, input, expected); -}