Use original field/method signatures in Proguard map

Change-Id: I10013606757b636b370c66fae7667287f43a5c50
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 16858af..bca3e00 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -490,6 +490,7 @@
         ClassNameMapper classNameMapper =
             LineNumberOptimizer.run(
                 application,
+                appView.getGraphLense(),
                 namingLens,
                 options.lineNumberOptimization == LineNumberOptimization.IDENTITY_MAPPING);
         timing.end();
diff --git a/src/main/java/com/android/tools/r8/graph/DexField.java b/src/main/java/com/android/tools/r8/graph/DexField.java
index 98c523d..dcab5ae 100644
--- a/src/main/java/com/android/tools/r8/graph/DexField.java
+++ b/src/main/java/com/android/tools/r8/graph/DexField.java
@@ -117,6 +117,10 @@
     return clazz;
   }
 
+  public String qualifiedName() {
+    return clazz + "." + name;
+  }
+
   @Override
   public String toSmaliString() {
     return clazz.toSmaliString() + "->" + name + ":" + type.toSmaliString();
diff --git a/src/main/java/com/android/tools/r8/naming/MemberNaming.java b/src/main/java/com/android/tools/r8/naming/MemberNaming.java
index c766c37..094dcaf 100644
--- a/src/main/java/com/android/tools/r8/naming/MemberNaming.java
+++ b/src/main/java/com/android/tools/r8/naming/MemberNaming.java
@@ -134,7 +134,12 @@
     }
 
     public static FieldSignature fromDexField(DexField field) {
-      return new FieldSignature(field.name.toSourceString(),
+      return fromDexField(field, false);
+    }
+
+    public static FieldSignature fromDexField(DexField field, boolean withQualifiedName) {
+      return new FieldSignature(
+          withQualifiedName ? field.qualifiedName() : field.name.toSourceString(),
           field.type.toSourceString());
     }
 
diff --git a/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java b/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
index 6d12230..66490b2 100644
--- a/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
+++ b/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
@@ -20,6 +20,7 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.GraphLense;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.ClassNaming;
@@ -35,6 +36,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.function.Function;
 import java.util.function.Supplier;
 
 public class LineNumberOptimizer {
@@ -202,7 +204,10 @@
   }
 
   public static ClassNameMapper run(
-      DexApplication application, NamingLens namingLens, boolean identityMapping) {
+      DexApplication application,
+      GraphLense graphLense,
+      NamingLens namingLens,
+      boolean identityMapping) {
     ClassNameMapper.Builder classNameMapperBuilder = ClassNameMapper.builder();
     // Collect which files contain which classes that need to have their line numbers optimized.
     for (DexProgramClass clazz : application.classes()) {
@@ -230,7 +235,7 @@
       addClassToClassNaming(clazz, renamedClassName, onDemandClassNamingBuilder);
 
       // First transfer renamed fields to classNamingBuilder.
-      addFieldsToClassNaming(namingLens, clazz, onDemandClassNamingBuilder);
+      addFieldsToClassNaming(graphLense, namingLens, clazz, onDemandClassNamingBuilder);
 
       // Then process the methods, ordered by renamed name.
       List<DexString> renamedMethodNames = new ArrayList<>(methodsByRenamedName.keySet());
@@ -261,7 +266,9 @@
             }
           }
 
-          MethodSignature originalSignature = MethodSignature.fromDexMethod(method.method);
+          DexMethod originalMethod = graphLense.getOriginalMethodSignature(method.method);
+          MethodSignature originalSignature =
+              MethodSignature.fromDexMethod(originalMethod, originalMethod.holder != clazz.type);
 
           DexString obfuscatedNameDexString = namingLens.lookupName(method.method);
           String obfuscatedName = obfuscatedNameDexString.toString();
@@ -269,7 +276,8 @@
           // Add simple "a() -> b" mapping if we won't have any other with concrete line numbers
           if (mappedPositions.isEmpty()) {
             // But only if it's been renamed.
-            if (obfuscatedNameDexString != method.method.name) {
+            if (obfuscatedNameDexString != originalMethod.name
+                || originalMethod.holder != clazz.type) {
               onDemandClassNamingBuilder
                   .get()
                   .addMappedRange(null, originalSignature, null, obfuscatedName);
@@ -278,7 +286,16 @@
           }
 
           Map<DexMethod, MethodSignature> signatures = new IdentityHashMap<>();
-          signatures.put(method.method, originalSignature);
+          signatures.put(originalMethod, originalSignature);
+          Function<DexMethod, MethodSignature> getOriginalMethodSignature =
+              m -> {
+                DexMethod original = graphLense.getOriginalMethodSignature(m);
+                return signatures.computeIfAbsent(
+                    original,
+                    key ->
+                        MethodSignature.fromDexMethod(
+                            original, original.holder != clazz.getType()));
+              };
 
           MemberNaming memberNaming = new MemberNaming(originalSignature, obfuscatedName);
           onDemandClassNamingBuilder.get().addMemberEntry(memberNaming);
@@ -309,23 +326,14 @@
             ClassNaming.Builder classNamingBuilder = onDemandClassNamingBuilder.get();
             classNamingBuilder.addMappedRange(
                 obfuscatedRange,
-                signatures.computeIfAbsent(
-                    firstPosition.method,
-                    m ->
-                        MethodSignature.fromDexMethod(
-                            m, firstPosition.method.holder != clazz.getType())),
+                getOriginalMethodSignature.apply(firstPosition.method),
                 originalRange,
                 obfuscatedName);
             Position caller = firstPosition.caller;
             while (caller != null) {
-              Position finalCaller = caller;
               classNamingBuilder.addMappedRange(
                   obfuscatedRange,
-                  signatures.computeIfAbsent(
-                      caller.method,
-                      m ->
-                          MethodSignature.fromDexMethod(
-                              m, finalCaller.method.holder != clazz.getType())),
+                  getOriginalMethodSignature.apply(caller.method),
                   Math.max(caller.line, 0), // Prevent against "no-position".
                   obfuscatedName);
               caller = caller.callerPosition;
@@ -381,16 +389,20 @@
     }
   }
 
-  private static void addFieldsToClassNaming(NamingLens namingLens, DexProgramClass clazz,
+  private static void addFieldsToClassNaming(
+      GraphLense graphLense,
+      NamingLens namingLens,
+      DexProgramClass clazz,
       Supplier<Builder> onDemandClassNamingBuilder) {
     clazz.forEachField(
         dexEncodedField -> {
           DexField dexField = dexEncodedField.field;
+          DexField originalField = graphLense.getOriginalFieldSignature(dexField);
           DexString renamedName = namingLens.lookupName(dexField);
-          if (renamedName != dexField.name) {
-            FieldSignature signature =
-                new FieldSignature(dexField.name.toString(), dexField.type.toString());
-            MemberNaming memberNaming = new MemberNaming(signature, renamedName.toString());
+          if (renamedName != originalField.name || originalField.clazz != clazz.type) {
+            FieldSignature originalSignature =
+                FieldSignature.fromDexField(originalField, originalField.clazz != clazz.type);
+            MemberNaming memberNaming = new MemberNaming(originalSignature, renamedName.toString());
             onDemandClassNamingBuilder.get().addMemberEntry(memberNaming);
           }
         });
diff --git a/src/test/examples/classmerging/ProguardFieldMapTest.java b/src/test/examples/classmerging/ProguardFieldMapTest.java
new file mode 100644
index 0000000..0cbf8a1
--- /dev/null
+++ b/src/test/examples/classmerging/ProguardFieldMapTest.java
@@ -0,0 +1,25 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package classmerging;
+
+public class ProguardFieldMapTest {
+
+  public static void main(String[] args) {
+    B b = new B();
+    b.test();
+  }
+
+  public static class A {
+
+    public String f = "A.f";
+  }
+
+  public static class B extends A {
+
+    public void test() {
+      System.out.println(f);
+    }
+  }
+}
diff --git a/src/test/examples/classmerging/ProguardMethodMapTest.java b/src/test/examples/classmerging/ProguardMethodMapTest.java
new file mode 100644
index 0000000..9ce0ad7
--- /dev/null
+++ b/src/test/examples/classmerging/ProguardMethodMapTest.java
@@ -0,0 +1,29 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package classmerging;
+
+public class ProguardMethodMapTest {
+
+  public static void main(String[] args) {
+    B b = new B();
+    b.method();
+  }
+
+  public static class A {
+
+    public void method() {
+      System.out.println("In A.method()");
+    }
+  }
+
+  public static class B extends A {
+
+    @Override
+    public void method() {
+      System.out.println("In B.method()");
+      super.method();
+    }
+  }
+}
diff --git a/src/test/examples/classmerging/keep-rules.txt b/src/test/examples/classmerging/keep-rules.txt
index 37a3010..3a70504 100644
--- a/src/test/examples/classmerging/keep-rules.txt
+++ b/src/test/examples/classmerging/keep-rules.txt
@@ -37,6 +37,12 @@
 -keep public class classmerging.PinnedParameterTypesTest$TestClass {
   public static void method(...);
 }
+-keep public class classmerging.ProguardFieldMapTest {
+  public static void main(...);
+}
+-keep public class classmerging.ProguardMethodMapTest {
+  public static void main(...);
+}
 -keep public class classmerging.SimpleInterfaceAccessTest {
   public static void main(...);
 }
diff --git a/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
index 6f3c97a..18e8c83 100644
--- a/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.OutputMode;
 import com.android.tools.r8.R8Command;
+import com.android.tools.r8.StringConsumer.FileConsumer;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
@@ -34,8 +35,11 @@
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.base.Joiner;
+import com.google.common.base.Predicates;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -45,6 +49,7 @@
 import java.util.concurrent.ExecutionException;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -97,7 +102,7 @@
   );
 
   @Test
-  public void testClassesHaveBeenMerged() throws Exception {
+  public void testClassesHaveBeenMerged() throws Throwable {
     runR8(EXAMPLE_KEEP, this::configure);
     // GenericInterface should be merged into GenericInterfaceImpl.
     for (String candidate : CAN_BE_MERGED) {
@@ -109,7 +114,7 @@
   }
 
   @Test
-  public void testClassesHaveNotBeenMerged() throws Exception {
+  public void testClassesHaveNotBeenMerged() throws Throwable {
     runR8(DONT_OPTIMIZE, null);
     for (String candidate : CAN_BE_MERGED) {
       assertTrue(inspector.clazz(candidate).isPresent());
@@ -124,7 +129,7 @@
   // Assuming that the chance of observing one of the two orderings is 50%, this test therefore has
   // approximately 3% chance of succeeding although there is an issue.
   @Test
-  public void testCallGraphCycle() throws Exception {
+  public void testCallGraphCycle() throws Throwable {
     String main = "classmerging.CallGraphCycleTest";
     Path[] programFiles =
         new Path[] {
@@ -140,7 +145,7 @@
   }
 
   @Test
-  public void testConflictInGeneratedName() throws Exception {
+  public void testConflictInGeneratedName() throws Throwable {
     String main = "classmerging.ConflictInGeneratedNameTest";
     Path[] programFiles =
         new Path[] {
@@ -204,14 +209,14 @@
   }
 
   @Test
-  public void testConflictWasDetected() throws Exception {
+  public void testConflictWasDetected() throws Throwable {
     runR8(EXAMPLE_KEEP, this::configure);
     assertTrue(inspector.clazz("classmerging.ConflictingInterface").isPresent());
     assertTrue(inspector.clazz("classmerging.ConflictingInterfaceImpl").isPresent());
   }
 
   @Test
-  public void testFieldCollision() throws Exception {
+  public void testFieldCollision() throws Throwable {
     String main = "classmerging.FieldCollisionTest";
     Path[] programFiles =
         new Path[] {
@@ -227,7 +232,7 @@
   }
 
   @Test
-  public void testLambdaRewriting() throws Exception {
+  public void testLambdaRewriting() throws Throwable {
     String main = "classmerging.LambdaRewritingTest";
     Path[] programFiles =
         new Path[] {
@@ -250,7 +255,7 @@
   }
 
   @Test
-  public void testMethodCollision() throws Exception {
+  public void testMethodCollision() throws Throwable {
     String main = "classmerging.MethodCollisionTest";
     Path[] programFiles =
         new Path[] {
@@ -275,7 +280,7 @@
   }
 
   @Test
-  public void testNestedDefaultInterfaceMethodsTest() throws Exception {
+  public void testNestedDefaultInterfaceMethodsTest() throws Throwable {
     String main = "classmerging.NestedDefaultInterfaceMethodsTest";
     Path[] programFiles =
         new Path[] {
@@ -294,7 +299,7 @@
   }
 
   @Test
-  public void testNestedDefaultInterfaceMethodsWithCDumpTest() throws Exception {
+  public void testNestedDefaultInterfaceMethodsWithCDumpTest() throws Throwable {
     String main = "classmerging.NestedDefaultInterfaceMethodsTest";
     Path[] programFiles =
         new Path[] {
@@ -319,7 +324,7 @@
   }
 
   @Test
-  public void testPinnedParameterTypes() throws Exception {
+  public void testPinnedParameterTypes() throws Throwable {
     String main = "classmerging.PinnedParameterTypesTest";
     Path[] programFiles =
         new Path[] {
@@ -342,7 +347,213 @@
   }
 
   @Test
-  public void testSuperCallWasDetected() throws Exception {
+  public void testProguardFieldMap() throws Throwable {
+    String main = "classmerging.ProguardFieldMapTest";
+    Path[] programFiles =
+        new Path[] {
+          CF_DIR.resolve("ProguardFieldMapTest.class"),
+          CF_DIR.resolve("ProguardFieldMapTest$A.class"),
+          CF_DIR.resolve("ProguardFieldMapTest$B.class")
+        };
+
+    // Try without vertical class merging, to check that output is as expected.
+    String expectedProguardMapWithoutClassMerging =
+        Joiner.on(System.lineSeparator())
+            .join(
+                "classmerging.ProguardFieldMapTest -> classmerging.ProguardFieldMapTest:",
+                "    1:2:void main(java.lang.String[]):10:11 -> main",
+                "classmerging.ProguardFieldMapTest$A -> classmerging.ProguardFieldMapTest$A:",
+                "    1:1:void <init>():14:14 -> <init>",
+                "    2:2:void <init>():16:16 -> <init>",
+                "classmerging.ProguardFieldMapTest$B -> classmerging.ProguardFieldMapTest$B:",
+                "    1:1:void <init>():19:19 -> <init>",
+                "    1:1:void test():22:22 -> test");
+    runTestOnInput(
+        main,
+        readProgramFiles(programFiles),
+        Predicates.alwaysTrue(),
+        getProguardConfig(EXAMPLE_KEEP),
+        options -> {
+          configure(options);
+          options.enableClassMerging = false;
+          options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
+          options.proguardMapConsumer =
+              (proguardMap, handler) ->
+                  assertThat(proguardMap, containsString(expectedProguardMapWithoutClassMerging));
+        });
+
+    // Try with vertical class merging.
+    String expectedProguardMapWithClassMerging =
+        Joiner.on(System.lineSeparator())
+            .join(
+                "classmerging.ProguardFieldMapTest -> classmerging.ProguardFieldMapTest:",
+                "    1:2:void main(java.lang.String[]):10:11 -> main",
+                "classmerging.ProguardFieldMapTest$B -> classmerging.ProguardFieldMapTest$B:",
+                "    java.lang.String classmerging.ProguardFieldMapTest$A.f -> f",
+                "    1:1:void classmerging.ProguardFieldMapTest$A.<init>():14:14 -> <init>",
+                "    1:1:void <init>():19 -> <init>",
+                "    2:2:void classmerging.ProguardFieldMapTest$A.<init>():16:16 -> <init>",
+                "    2:2:void <init>():19 -> <init>",
+                "    1:1:void test():22:22 -> test");
+    Set<String> preservedClassNames =
+        ImmutableSet.of("classmerging.ProguardFieldMapTest", "classmerging.ProguardFieldMapTest$B");
+    runTestOnInput(
+        main,
+        readProgramFiles(programFiles),
+        preservedClassNames::contains,
+        getProguardConfig(EXAMPLE_KEEP),
+        options -> {
+          configure(options);
+          options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
+          options.proguardMapConsumer =
+              (proguardMap, handler) ->
+                  assertThat(proguardMap, containsString(expectedProguardMapWithClassMerging));
+        });
+  }
+
+  @Test
+  public void testProguardMethodMap() throws Throwable {
+    String main = "classmerging.ProguardMethodMapTest";
+    Path[] programFiles =
+        new Path[] {
+          CF_DIR.resolve("ProguardMethodMapTest.class"),
+          CF_DIR.resolve("ProguardMethodMapTest$A.class"),
+          CF_DIR.resolve("ProguardMethodMapTest$B.class")
+        };
+
+    // Try without vertical class merging, to check that output is as expected.
+    String expectedProguardMapWithoutClassMerging =
+        Joiner.on(System.lineSeparator())
+            .join(
+                "classmerging.ProguardMethodMapTest -> classmerging.ProguardMethodMapTest:",
+                "    1:2:void main(java.lang.String[]):10:11 -> main",
+                "classmerging.ProguardMethodMapTest$A -> classmerging.ProguardMethodMapTest$A:",
+                "    1:1:void <init>():14:14 -> <init>",
+                "    1:1:void method():17:17 -> method",
+                "classmerging.ProguardMethodMapTest$B -> classmerging.ProguardMethodMapTest$B:",
+                "    1:1:void <init>():21:21 -> <init>",
+                "    1:2:void method():25:26 -> method");
+    runTestOnInput(
+        main,
+        readProgramFiles(programFiles),
+        Predicates.alwaysTrue(),
+        getProguardConfig(EXAMPLE_KEEP),
+        options -> {
+          configure(options);
+          options.enableClassMerging = false;
+          options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
+          options.proguardMapConsumer =
+              (proguardMap, handler) ->
+                  assertThat(proguardMap, containsString(expectedProguardMapWithoutClassMerging));
+        });
+
+    // Try with vertical class merging.
+    String expectedProguardMapWithClassMerging =
+        Joiner.on(System.lineSeparator())
+            .join(
+                "classmerging.ProguardMethodMapTest -> classmerging.ProguardMethodMapTest:",
+                "    1:2:void main(java.lang.String[]):10:11 -> main",
+                "classmerging.ProguardMethodMapTest$B -> classmerging.ProguardMethodMapTest$B:",
+                // TODO(christofferqa): Should this be "...<init>():14 -> <init>" to reflect that
+                // A.<init> has been inlined into B.<init>?
+                "    1:1:void classmerging.ProguardMethodMapTest$A.<init>():14:14 -> <init>",
+                // TODO(christofferqa): Should this be " ...<init>():21:21 -> <init>"?
+                "    1:1:void <init>():21 -> <init>",
+                "    1:2:void method():25:26 -> method",
+                "    1:1:void classmerging.ProguardMethodMapTest$A.method():17:17 -> "
+                    + "method$classmerging$ProguardMethodMapTest$A");
+    Set<String> preservedClassNames =
+        ImmutableSet.of(
+            "classmerging.ProguardMethodMapTest", "classmerging.ProguardMethodMapTest$B");
+    runTestOnInput(
+        main,
+        readProgramFiles(programFiles),
+        preservedClassNames::contains,
+        getProguardConfig(EXAMPLE_KEEP),
+        options -> {
+          configure(options);
+          options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
+          options.proguardMapConsumer =
+              (proguardMap, handler) ->
+                  assertThat(proguardMap, containsString(expectedProguardMapWithClassMerging));
+        });
+  }
+
+  // TODO(christofferqa): The sets appInfo.forceInline and appInfo.neverInline must be rewritten
+  // with the graph lense after vertical class merging.
+  @Ignore
+  @Test
+  public void testProguardMethodMapAfterInlining() throws Throwable {
+    String main = "classmerging.ProguardMethodMapTest";
+    Path[] programFiles =
+        new Path[] {
+          CF_DIR.resolve("ProguardMethodMapTest.class"),
+          CF_DIR.resolve("ProguardMethodMapTest$A.class"),
+          CF_DIR.resolve("ProguardMethodMapTest$B.class")
+        };
+
+    // Try without vertical class merging, to check that output is as expected.
+    String expectedProguardMapWithoutClassMerging =
+        Joiner.on(System.lineSeparator())
+            .join(
+                "classmerging.ProguardMethodMapTest -> classmerging.ProguardMethodMapTest:",
+                "    1:2:void main(java.lang.String[]):10:11 -> main",
+                "classmerging.ProguardMethodMapTest$A -> classmerging.ProguardMethodMapTest$A:",
+                "    1:1:void <init>():14:14 -> <init>",
+                "classmerging.ProguardMethodMapTest$B -> classmerging.ProguardMethodMapTest$B:",
+                "    1:1:void <init>():21:21 -> <init>",
+                "    1:1:void method():25:25 -> method",
+                "    2:2:void classmerging.ProguardMethodMapTest$A.method():17:17 -> method",
+                "    2:2:void method():26 -> method");
+    runTestOnInput(
+        main,
+        readProgramFiles(programFiles),
+        Predicates.alwaysTrue(),
+        getProguardConfig(
+            EXAMPLE_KEEP,
+            "-forceinline class classmerging.ProguardMethodMapTest$A { public void method(); }"),
+        options -> {
+          configure(options);
+          options.enableClassMerging = false;
+          options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
+          options.proguardMapConsumer =
+              (proguardMap, handler) ->
+                  assertThat(proguardMap, containsString(expectedProguardMapWithoutClassMerging));
+        });
+
+    // Try with vertical class merging.
+    String expectedProguardMapWithClassMerging =
+        Joiner.on(System.lineSeparator())
+            .join(
+                "classmerging.ProguardMethodMapTest -> classmerging.ProguardMethodMapTest:",
+                "    1:2:void main(java.lang.String[]):10:11 -> main",
+                "classmerging.ProguardMethodMapTest$B -> classmerging.ProguardMethodMapTest$B:",
+                "    1:1:void classmerging.ProguardMethodMapTest$A.<init>():14:14 -> <init>",
+                "    1:1:void <init>():21 -> <init>",
+                "    1:1:void method():25:25 -> method",
+                "    2:2:void classmerging.ProguardMethodMapTest$A.method():17:17 -> method",
+                "    2:2:void method():26 -> method");
+    Set<String> preservedClassNames =
+        ImmutableSet.of(
+            "classmerging.ProguardMethodMapTest", "classmerging.ProguardMethodMapTest$B");
+    runTestOnInput(
+        main,
+        readProgramFiles(programFiles),
+        preservedClassNames::contains,
+        getProguardConfig(
+            EXAMPLE_KEEP,
+            "-forceinline class classmerging.ProguardMethodMapTest$A { public void method(); }"),
+        options -> {
+          configure(options);
+          options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
+          options.proguardMapConsumer =
+              (proguardMap, handler) ->
+                  assertThat(proguardMap, containsString(expectedProguardMapWithClassMerging));
+        });
+  }
+
+  @Test
+  public void testSuperCallWasDetected() throws Throwable {
     String main = "classmerging.SuperCallRewritingTest";
     Path[] programFiles =
         new Path[] {
@@ -366,7 +577,7 @@
   // targets SubClassThatReferencesSuperMethod.referencedMethod. When running without class
   // merging, R8 should not rewrite the invoke-super instruction into invoke-direct.
   @Test
-  public void testSuperCallNotRewrittenToDirect() throws Exception {
+  public void testSuperCallNotRewrittenToDirect() throws Throwable {
     String main = "classmerging.SuperCallRewritingTest";
     Path[] programFiles =
         new Path[] {
@@ -446,7 +657,7 @@
   //   }
   @Test
   @IgnoreForRangeOfVmVersions(from = Version.V5_1_1, to = Version.V6_0_1)
-  public void testSuperCallToMergedClassIsRewritten() throws Exception {
+  public void testSuperCallToMergedClassIsRewritten() throws Throwable {
     String main = "classmerging.SuperCallToMergedClassIsRewrittenTest";
     Set<String> preservedClassNames =
         ImmutableSet.of(
@@ -569,7 +780,7 @@
   }
 
   @Test
-  public void testConflictingInterfaceSignatures() throws Exception {
+  public void testConflictingInterfaceSignatures() throws Throwable {
     String main = "classmerging.ConflictingInterfaceSignaturesTest";
     Path[] programFiles =
         new Path[] {
@@ -588,7 +799,7 @@
   // If an exception class A is merged into another exception class B, then all exception tables
   // should be updated, and class A should be removed entirely.
   @Test
-  public void testExceptionTables() throws Exception {
+  public void testExceptionTables() throws Throwable {
     String main = "classmerging.ExceptionTest";
     Path[] programFiles =
         new Path[] {
@@ -624,7 +835,7 @@
   }
 
   @Test
-  public void testMergeDefaultMethodIntoClass() throws Exception {
+  public void testMergeDefaultMethodIntoClass() throws Throwable {
     String main = "classmerging.MergeDefaultMethodIntoClassTest";
     Path[] programFiles =
         new Path[] {
@@ -658,7 +869,7 @@
   }
 
   @Test
-  public void testNativeMethod() throws Exception {
+  public void testNativeMethod() throws Throwable {
     String main = "classmerging.ClassWithNativeMethodTest";
     Path[] programFiles =
         new Path[] {
@@ -676,7 +887,7 @@
   }
 
   @Test
-  public void testNoIllegalClassAccess() throws Exception {
+  public void testNoIllegalClassAccess() throws Throwable {
     String main = "classmerging.SimpleInterfaceAccessTest";
     Path[] programFiles =
         new Path[] {
@@ -701,7 +912,7 @@
   }
 
   @Test
-  public void testNoIllegalClassAccessWithAccessModifications() throws Exception {
+  public void testNoIllegalClassAccessWithAccessModifications() throws Throwable {
     // If access modifications are allowed then SimpleInterface should be merged into
     // SimpleInterfaceImpl.
     String main = "classmerging.SimpleInterfaceAccessTest";
@@ -736,7 +947,7 @@
   // into an invoke-direct instruction after it gets merged into class C. We should add a test that
   // checks that this works with and without inlining of the method B.m().
   @Test
-  public void testRewritePinnedMethod() throws Exception {
+  public void testRewritePinnedMethod() throws Throwable {
     String main = "classmerging.RewritePinnedMethodTest";
     Path[] programFiles =
         new Path[] {
@@ -759,7 +970,7 @@
   }
 
   @Test
-  public void testTemplateMethodPattern() throws Exception {
+  public void testTemplateMethodPattern() throws Throwable {
     String main = "classmerging.TemplateMethodTest";
     Path[] programFiles =
         new Path[] {
@@ -774,7 +985,7 @@
   }
 
   private CodeInspector runTest(
-      String main, Path[] programFiles, Predicate<String> preservedClassNames) throws Exception {
+      String main, Path[] programFiles, Predicate<String> preservedClassNames) throws Throwable {
     return runTest(main, programFiles, preservedClassNames, getProguardConfig(EXAMPLE_KEEP));
   }
 
@@ -783,14 +994,14 @@
       Path[] programFiles,
       Predicate<String> preservedClassNames,
       String proguardConfig)
-      throws Exception {
+      throws Throwable {
     return runTestOnInput(
         main, readProgramFiles(programFiles), preservedClassNames, proguardConfig);
   }
 
   private CodeInspector runTestOnInput(
       String main, AndroidApp input, Predicate<String> preservedClassNames, String proguardConfig)
-      throws Exception {
+      throws Throwable {
     return runTestOnInput(main, input, preservedClassNames, proguardConfig, this::configure);
   }
 
@@ -800,8 +1011,20 @@
       Predicate<String> preservedClassNames,
       String proguardConfig,
       Consumer<InternalOptions> optionsConsumer)
-      throws Exception {
-    AndroidApp output = compileWithR8(input, proguardConfig, optionsConsumer);
+      throws Throwable {
+    Path proguardMapPath = File.createTempFile("mapping", ".txt", temp.getRoot()).toPath();
+    R8Command.Builder commandBuilder =
+        ToolHelper.prepareR8CommandBuilder(input)
+            .addProguardConfiguration(ImmutableList.of(proguardConfig), Origin.unknown());
+    ToolHelper.allowTestProguardOptions(commandBuilder);
+    AndroidApp output =
+        ToolHelper.runR8(
+            commandBuilder.build(),
+            options -> {
+              optionsConsumer.accept(options);
+              options.proguardMapConsumer =
+                  new FileConsumer(proguardMapPath, options.proguardMapConsumer);
+            });
     CodeInspector inputInspector = new CodeInspector(input);
     CodeInspector outputInspector = new CodeInspector(output);
     // Check that all classes in [preservedClassNames] are in fact preserved.