Allow for using minification with applymapping

This will add a test ensuring that all classes and members that do not
have an explicit mapping will be minified depending on them being kept
or not.

The internal state of MethodNamingState that previously kept track of
reserved names and new name indices are now split in much the same way
as the field minification state. If this is not done, the assertion
that a child will only use indicies greater than its parent will not
hold.

Bug: 130736358
Change-Id: Ia7b9eef1a9ae7237f2ff17d9f1aef39a3c968a51
diff --git a/src/main/java/com/android/tools/r8/naming/MethodNameMinifier.java b/src/main/java/com/android/tools/r8/naming/MethodNameMinifier.java
index ba458bc..9b07115 100644
--- a/src/main/java/com/android/tools/r8/naming/MethodNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/MethodNameMinifier.java
@@ -37,7 +37,8 @@
  * <p>As in the Dalvik VM method dispatch takes argument and return types of methods into account,
  * we can further reuse names if the prototypes of two methods differ. For this, we store the above
  * state separately for each proto using a map from protos to {@link
- * MethodNamingState.InternalState} objects. These internal state objects are also linked.
+ * MethodNamingState.InternalReservationState} objects. These internal state objects are also
+ * linked.
  *
  * <p>Name assignment happens in 4 stages. In the first stage, we record all names that are used by
  * library classes or are flagged using a keep rule as reserved. This step also allocates the {@link
diff --git a/src/main/java/com/android/tools/r8/naming/MethodNamingState.java b/src/main/java/com/android/tools/r8/naming/MethodNamingState.java
index 6b48304..db8a84b 100644
--- a/src/main/java/com/android/tools/r8/naming/MethodNamingState.java
+++ b/src/main/java/com/android/tools/r8/naming/MethodNamingState.java
@@ -16,7 +16,8 @@
 class MethodNamingState<KeyType> {
 
   private final MethodNamingState<KeyType> parent;
-  private final Map<KeyType, InternalState> usedNames = new HashMap<>();
+  private final Map<KeyType, InternalReservationState> usedNames = new HashMap<>();
+  private final Map<KeyType, InternalNewNameState> newNameStates = new HashMap<>();
   private final Function<DexProto, KeyType> keyTransform;
   private final MemberNamingStrategy strategy;
 
@@ -38,27 +39,48 @@
     return new MethodNamingState<>(this, keyTransform, strategy);
   }
 
-  private InternalState findInternalStateFor(KeyType key) {
-    InternalState result = usedNames.get(key);
+  private InternalReservationState findInternalReservationStateFor(KeyType key) {
+    InternalReservationState result = usedNames.get(key);
     if (result == null && parent != null) {
-      result = parent.findInternalStateFor(key);
+      result = parent.findInternalReservationStateFor(key);
     }
     return result;
   }
 
-  private InternalState getOrCreateInternalStateFor(KeyType key) {
-    // TODO(herhut): Maybe allocate these sparsely and search via state chain.
-    InternalState result = usedNames.get(key);
+  private InternalReservationState getOrCreateInternalReservationStateFor(KeyType key) {
+    InternalReservationState result = usedNames.get(key);
     if (result == null) {
-      InternalState parentState = parent != null ? parent.getOrCreateInternalStateFor(key) : null;
-      result = new InternalState(parentState);
+      InternalReservationState parentState =
+          parent != null ? parent.getOrCreateInternalReservationStateFor(key) : null;
+      result = new InternalReservationState(parentState);
       usedNames.put(key, result);
     }
     return result;
   }
 
+  private InternalNewNameState findInternalNewNameStateFor(KeyType key) {
+    InternalNewNameState result = newNameStates.get(key);
+    if (result == null && parent != null) {
+      result = parent.findInternalNewNameStateFor(key);
+    }
+    return result;
+  }
+
+  private InternalNewNameState getOrCreateNewNameStateFor(KeyType key) {
+    InternalNewNameState result = newNameStates.get(key);
+    if (result == null) {
+      InternalReservationState reservationState = getOrCreateInternalReservationStateFor(key);
+      assert reservationState != null;
+      InternalNewNameState parentState =
+          parent != null ? parent.getOrCreateNewNameStateFor(key) : null;
+      result = new InternalNewNameState(parentState, reservationState);
+      newNameStates.put(key, result);
+    }
+    return result;
+  }
+
   private DexString getAssignedNameFor(DexString name, KeyType key) {
-    InternalState state = findInternalStateFor(key);
+    InternalReservationState state = findInternalReservationStateFor(key);
     if (state == null) {
       return null;
     }
@@ -69,7 +91,7 @@
     KeyType key = keyTransform.apply(proto);
     DexString result = getAssignedNameFor(original, key);
     if (result == null) {
-      InternalState state = getOrCreateInternalStateFor(key);
+      InternalNewNameState state = getOrCreateNewNameStateFor(key);
       result = state.getNewNameFor(source);
     }
     return result;
@@ -77,13 +99,13 @@
 
   void reserveName(DexString name, DexProto proto, DexString originalName) {
     KeyType key = keyTransform.apply(proto);
-    InternalState state = getOrCreateInternalStateFor(key);
+    InternalReservationState state = getOrCreateInternalReservationStateFor(key);
     state.reserveName(name, originalName);
   }
 
   boolean isReserved(DexString name, DexProto proto) {
     KeyType key = keyTransform.apply(proto);
-    InternalState state = findInternalStateFor(key);
+    InternalReservationState state = findInternalReservationStateFor(key);
     if (state == null) {
       return false;
     }
@@ -92,7 +114,7 @@
 
   DexString getReservedOriginalName(DexString name, DexProto proto) {
     KeyType key = keyTransform.apply(proto);
-    InternalState state = findInternalStateFor(key);
+    InternalReservationState state = findInternalReservationStateFor(key);
     if (state == null) {
       return null;
     }
@@ -101,7 +123,7 @@
 
   boolean isAvailable(DexProto proto, DexString candidate) {
     KeyType key = keyTransform.apply(proto);
-    InternalState state = findInternalStateFor(key);
+    InternalReservationState state = findInternalReservationStateFor(key);
     if (state == null) {
       return true;
     }
@@ -110,7 +132,7 @@
 
   void addRenaming(DexString original, DexProto proto, DexString newName) {
     KeyType key = keyTransform.apply(proto);
-    InternalState state = getOrCreateInternalStateFor(key);
+    InternalReservationState state = getOrCreateInternalReservationStateFor(key);
     state.addRenaming(original, newName);
   }
 
@@ -120,7 +142,7 @@
       String indentation,
       PrintStream out) {
     KeyType key = keyTransform.apply(proto);
-    InternalState state = getOrCreateInternalStateFor(key);
+    InternalNewNameState state = getOrCreateNewNameStateFor(key);
     out.print(indentation);
     out.print("NamingState(node=`");
     out.print(stateKeyGetter.apply(this).toSourceString());
@@ -137,34 +159,21 @@
     }
   }
 
-  class InternalState implements InternalNamingState {
-
-    private static final int INITIAL_NAME_COUNT = 1;
-    private static final int INITIAL_DICTIONARY_INDEX = 0;
-
-    private final InternalState parentInternalState;
+  class InternalReservationState {
+    private final InternalReservationState parentInternalState;
     private Map<DexString, DexString> reservedNames = null;
     private Map<DexString, DexString> renamings = null;
-    private int virtualNameCount;
-    private int directNameCount = 0;
-    private int dictionaryIndex;
 
-    private InternalState(InternalState parentInternalState) {
+    private InternalReservationState(InternalReservationState parentInternalState) {
       this.parentInternalState = parentInternalState;
-      this.dictionaryIndex =
-          parentInternalState == null
-              ? INITIAL_DICTIONARY_INDEX
-              : parentInternalState.dictionaryIndex;
-      this.virtualNameCount =
-          parentInternalState == null ? INITIAL_NAME_COUNT : parentInternalState.virtualNameCount;
     }
 
-    private boolean isReserved(DexString name) {
+    boolean isReserved(DexString name) {
       return (reservedNames != null && reservedNames.containsKey(name))
           || (parentInternalState != null && parentInternalState.isReserved(name));
     }
 
-    private DexString getReservedOriginalName(DexString name) {
+    DexString getReservedOriginalName(DexString name) {
       DexString result = null;
       if (reservedNames != null) {
         result = reservedNames.get(name);
@@ -175,6 +184,17 @@
       return result;
     }
 
+    DexString getAssignedNameFor(DexString original) {
+      DexString result = null;
+      if (renamings != null) {
+        result = renamings.get(original);
+      }
+      if (result == null && parentInternalState != null) {
+        result = parentInternalState.getAssignedNameFor(original);
+      }
+      return result;
+    }
+
     private boolean isAvailable(DexString name) {
       return !(renamings != null && renamings.containsValue(name))
           && !(reservedNames != null && reservedNames.containsKey(name))
@@ -188,57 +208,6 @@
       reservedNames.put(name, originalName);
     }
 
-    @Override
-    public int getDictionaryIndex() {
-      return dictionaryIndex;
-    }
-
-    @Override
-    public int incrementDictionaryIndex() {
-      return dictionaryIndex++;
-    }
-
-    private boolean checkParentPublicNameCountIsLessThanOrEqual() {
-      int maxParentCount = 0;
-      InternalState tmp = parentInternalState;
-      while (tmp != null) {
-        maxParentCount = Math.max(tmp.virtualNameCount, maxParentCount);
-        tmp = tmp.parentInternalState;
-      }
-      assert maxParentCount <= virtualNameCount;
-      return true;
-    }
-
-    @Override
-    public int incrementNameIndex(boolean isDirectMethodCall) {
-      assert checkParentPublicNameCountIsLessThanOrEqual();
-      if (isDirectMethodCall) {
-        return virtualNameCount + directNameCount++;
-      } else {
-        assert directNameCount == 0;
-        return virtualNameCount++;
-      }
-    }
-
-    DexString getAssignedNameFor(DexString original) {
-      DexString result = null;
-      if (renamings != null) {
-        result = renamings.get(original);
-      }
-      if (result == null && parentInternalState != null) {
-        result = parentInternalState.getAssignedNameFor(original);
-      }
-      return result;
-    }
-
-    private DexString getNewNameFor(DexMethod source) {
-      DexString name;
-      do {
-        name = strategy.next(source, this);
-      } while (!isAvailable(name));
-      return name;
-    }
-
     void addRenaming(DexString original, DexString newName) {
       if (renamings == null) {
         renamings = new HashMap<>();
@@ -246,47 +215,6 @@
       renamings.put(original, newName);
     }
 
-    void printInternalState(
-        MethodNamingState<?> expectedNamingState,
-        Function<MethodNamingState<?>, DexType> stateKeyGetter,
-        String indentation,
-        PrintStream out) {
-      assert expectedNamingState == MethodNamingState.this;
-
-      DexType stateKey = stateKeyGetter.apply(expectedNamingState);
-      out.print(indentation);
-      out.print("InternalState(node=`");
-      out.print(stateKey != null ? stateKey.toSourceString() : "<GLOBAL>");
-      out.println("`)");
-
-      printLastName(indentation + "  ", out);
-      printReservedNames(indentation + "  ", out);
-      printRenamings(indentation + "  ", out);
-
-      if (parentInternalState != null) {
-        parentInternalState.printInternalState(
-            expectedNamingState.parent, stateKeyGetter, indentation + "  ", out);
-      }
-    }
-
-    void printLastName(String indentation, PrintStream out) {
-      out.print(indentation);
-      out.print("Last name: ");
-      int index = virtualNameCount + directNameCount;
-      if (index > 1) {
-        out.print(StringUtils.numberToIdentifier(index - 1));
-        out.print(" (public name count: ");
-        out.print(virtualNameCount);
-        out.print(")");
-        out.print(" (direct name count: ");
-        out.print(directNameCount);
-        out.print(")");
-      } else {
-        out.print("<NONE>");
-      }
-      out.println();
-    }
-
     void printReservedNames(String indentation, PrintStream out) {
       out.print(indentation);
       out.print("Reserved names:");
@@ -321,4 +249,111 @@
       out.println();
     }
   }
+
+  class InternalNewNameState implements InternalNamingState {
+
+    private final InternalNewNameState parentInternalState;
+    private final InternalReservationState reservationState;
+
+    private static final int INITIAL_NAME_COUNT = 1;
+    private static final int INITIAL_DICTIONARY_INDEX = 0;
+
+    private int virtualNameCount;
+    private int directNameCount = 0;
+    private int dictionaryIndex;
+
+    private InternalNewNameState(
+        InternalNewNameState parentInternalState, InternalReservationState reservationState) {
+      this.parentInternalState = parentInternalState;
+      this.reservationState = reservationState;
+      this.dictionaryIndex =
+          parentInternalState == null
+              ? INITIAL_DICTIONARY_INDEX
+              : parentInternalState.dictionaryIndex;
+      this.virtualNameCount =
+          parentInternalState == null ? INITIAL_NAME_COUNT : parentInternalState.virtualNameCount;
+      assert reservationState != null;
+    }
+
+    @Override
+    public int getDictionaryIndex() {
+      return dictionaryIndex;
+    }
+
+    @Override
+    public int incrementDictionaryIndex() {
+      return dictionaryIndex++;
+    }
+
+    private boolean checkParentPublicNameCountIsLessThanOrEqual() {
+      int maxParentCount = 0;
+      InternalNewNameState tmp = parentInternalState;
+      while (tmp != null) {
+        maxParentCount = Math.max(tmp.virtualNameCount, maxParentCount);
+        tmp = tmp.parentInternalState;
+      }
+      assert maxParentCount <= virtualNameCount;
+      return true;
+    }
+
+    @Override
+    public int incrementNameIndex(boolean isDirectMethodCall) {
+      assert checkParentPublicNameCountIsLessThanOrEqual();
+      if (isDirectMethodCall) {
+        return virtualNameCount + directNameCount++;
+      } else {
+        assert directNameCount == 0;
+        return virtualNameCount++;
+      }
+    }
+
+    private DexString getNewNameFor(DexMethod source) {
+      DexString name;
+      do {
+        name = strategy.next(source, this);
+      } while (!reservationState.isAvailable(name));
+      return name;
+    }
+
+    void printInternalState(
+        MethodNamingState<?> expectedNamingState,
+        Function<MethodNamingState<?>, DexType> stateKeyGetter,
+        String indentation,
+        PrintStream out) {
+      assert expectedNamingState == MethodNamingState.this;
+
+      DexType stateKey = stateKeyGetter.apply(expectedNamingState);
+      out.print(indentation);
+      out.print("InternalState(node=`");
+      out.print(stateKey != null ? stateKey.toSourceString() : "<GLOBAL>");
+      out.println("`)");
+
+      printLastName(indentation + "  ", out);
+      reservationState.printReservedNames(indentation + "  ", out);
+      reservationState.printRenamings(indentation + "  ", out);
+
+      if (parentInternalState != null) {
+        parentInternalState.printInternalState(
+            expectedNamingState.parent, stateKeyGetter, indentation + "  ", out);
+      }
+    }
+
+    void printLastName(String indentation, PrintStream out) {
+      out.print(indentation);
+      out.print("Last name: ");
+      int index = virtualNameCount + directNameCount;
+      if (index > 1) {
+        out.print(StringUtils.numberToIdentifier(index - 1));
+        out.print(" (public name count: ");
+        out.print(virtualNameCount);
+        out.print(")");
+        out.print(" (direct name count: ");
+        out.print(directNameCount);
+        out.print(")");
+      } else {
+        out.print("<NONE>");
+      }
+      out.println();
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/naming/Minifier.java b/src/main/java/com/android/tools/r8/naming/Minifier.java
index a3d15a2..b86cfc4 100644
--- a/src/main/java/com/android/tools/r8/naming/Minifier.java
+++ b/src/main/java/com/android/tools/r8/naming/Minifier.java
@@ -116,7 +116,7 @@
   static class MinificationClassNamingStrategy extends BaseMinificationNamingStrategy
       implements ClassNamingStrategy {
 
-    private final AppView<?> appView;
+    final AppView<?> appView;
     private final DexItemFactory factory;
 
     MinificationClassNamingStrategy(AppView<?> appView) {
@@ -156,10 +156,9 @@
   static class MinifierMemberNamingStrategy extends BaseMinificationNamingStrategy
       implements MemberNamingStrategy {
 
+    final AppView<?> appView;
     private final DexItemFactory factory;
 
-    private final AppView<?> appView;
-
     public MinifierMemberNamingStrategy(AppView<?> appView) {
       super(appView.options().getProguardConfiguration().getObfuscationDictionary());
       this.appView = appView;
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapMinifier.java b/src/main/java/com/android/tools/r8/naming/ProguardMapMinifier.java
index 6efddeb..895b8c5 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapMinifier.java
@@ -16,14 +16,15 @@
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.desugar.InterfaceMethodRewriter;
-import com.android.tools.r8.naming.ClassNameMinifier.ClassNamingStrategy;
 import com.android.tools.r8.naming.ClassNameMinifier.ClassRenaming;
 import com.android.tools.r8.naming.FieldNameMinifier.FieldRenaming;
 import com.android.tools.r8.naming.MemberNaming.FieldSignature;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.naming.MemberNaming.Signature;
 import com.android.tools.r8.naming.MethodNameMinifier.MethodRenaming;
+import com.android.tools.r8.naming.Minifier.MinificationClassNamingStrategy;
 import com.android.tools.r8.naming.Minifier.MinificationPackageNamingStrategy;
+import com.android.tools.r8.naming.Minifier.MinifierMemberNamingStrategy;
 import com.android.tools.r8.position.Position;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.Reporter;
@@ -110,7 +111,7 @@
     ClassNameMinifier classNameMinifier =
         new ClassNameMinifier(
             appView,
-            new ApplyMappingClassNamingStrategy(mappedNames),
+            new ApplyMappingClassNamingStrategy(appView, mappedNames),
             // The package naming strategy will actually not be used since all classes and methods
             // will be output with identity name if not found in mapping. However, there is a check
             // in the ClassNameMinifier that the strategy should produce a "fresh" name so we just
@@ -322,27 +323,41 @@
     }
   }
 
-  static class ApplyMappingClassNamingStrategy implements ClassNamingStrategy {
+  static class ApplyMappingClassNamingStrategy extends MinificationClassNamingStrategy {
 
     private final Map<DexType, DexString> mappings;
+    private final boolean isMinifying;
 
-    ApplyMappingClassNamingStrategy(Map<DexType, DexString> mappings) {
+    ApplyMappingClassNamingStrategy(AppView<?> appView, Map<DexType, DexString> mappings) {
+      super(appView);
       this.mappings = mappings;
+      this.isMinifying = appView.options().isMinifying();
     }
 
     @Override
     public DexString next(DexType type, char[] packagePrefix, InternalNamingState state) {
-      return mappings.getOrDefault(type, type.descriptor);
+      DexString nextName = mappings.get(type);
+      if (nextName != null) {
+        return nextName;
+      }
+      assert !(isMinifying && noObfuscation(type));
+      return isMinifying ? super.next(type, packagePrefix, state) : type.descriptor;
     }
 
     @Override
     public boolean noObfuscation(DexType type) {
-      // We have an explicit mapping from the proguard map thus everything might have to be renamed.
-      return false;
+      if (mappings.containsKey(type)) {
+        return false;
+      }
+      DexClass dexClass = appView.definitionFor(type);
+      if (dexClass == null || dexClass.isNotProgramClass()) {
+        return true;
+      }
+      return super.noObfuscation(type);
     }
   }
 
-  static class ApplyMappingMemberNamingStrategy implements MemberNamingStrategy {
+  static class ApplyMappingMemberNamingStrategy extends MinifierMemberNamingStrategy {
 
     private final Map<DexReference, MemberNaming> mappedNames;
     private final DexItemFactory factory;
@@ -350,6 +365,7 @@
 
     public ApplyMappingMemberNamingStrategy(
         AppView<?> appView, Map<DexReference, MemberNaming> mappedNames) {
+      super(appView);
       this.mappedNames = mappedNames;
       this.factory = appView.dexItemFactory();
       this.reporter = appView.options().reporter;
@@ -358,13 +374,24 @@
     @Override
     public DexString next(DexMethod method, InternalNamingState internalState) {
       assert !mappedNames.containsKey(method);
-      return method.name;
+      return canMinify(method, method.holder) ? super.next(method, internalState) : method.name;
     }
 
     @Override
     public DexString next(DexField field, InternalNamingState internalState) {
       assert !mappedNames.containsKey(field);
-      return field.name;
+      return canMinify(field, field.holder) ? super.next(field, internalState) : field.name;
+    }
+
+    private boolean canMinify(DexReference reference, DexType type) {
+      if (!appView.options().isMinifying()) {
+        return false;
+      }
+      DexClass dexClass = appView.definitionFor(type);
+      if (dexClass == null || dexClass.isNotProgramClass()) {
+        return false;
+      }
+      return appView.rootSet().mayBeMinified(reference, appView);
     }
 
     @Override
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingMinificationTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingMinificationTest.java
new file mode 100644
index 0000000..b920732
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingMinificationTest.java
@@ -0,0 +1,133 @@
+// Copyright (c) 2019, 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 com.android.tools.r8.naming.applymapping;
+
+import static com.android.tools.r8.references.Reference.methodFromMethod;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static junit.framework.TestCase.assertTrue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.FieldSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.io.IOException;
+import java.util.concurrent.ExecutionException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ApplyMappingMinificationTest extends TestBase {
+
+  @NeverClassInline
+  public static class A {
+    public int fieldA = 1;
+    public int fieldB = 2;
+
+    @NeverInline
+    public void methodA() {
+      System.out.println("A.methodA");
+    }
+
+    @NeverInline
+    public void methodB() {
+      System.out.println("A.methodB");
+    }
+
+    @NeverInline
+    public void methodC() {
+      System.out.println("A.methodC");
+    }
+  }
+
+  @NeverClassInline
+  public static class B {
+    @NeverInline
+    public void foo() {
+      System.out.println("B.foo");
+    }
+  }
+
+  public static class C {
+
+    public static void main(String[] args) {
+      System.out.println(new A().fieldA);
+      System.out.println(new A().fieldB);
+      new A().methodA();
+      new A().methodB();
+      new A().methodC();
+      new B().foo();
+    }
+  }
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().build();
+  }
+
+  public ApplyMappingMinificationTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testApplyMappingFollowedByMinification()
+      throws IOException, CompilationFailedException, ExecutionException, NoSuchMethodException {
+    String[] pgMap =
+        new String[] {
+          A.class.getTypeName() + " -> a:", "  int fieldA -> a", "  void methodA() -> a"
+        };
+    R8TestRunResult runResult =
+        testForR8(parameters.getBackend())
+            .addInnerClasses(ApplyMappingMinificationTest.class)
+            .addKeepMainRule(C.class)
+            .addKeepMethodRules(methodFromMethod(A.class.getDeclaredMethod("methodC")))
+            .enableInliningAnnotations()
+            .enableClassInliningAnnotations()
+            .addApplyMapping(StringUtils.lines(pgMap))
+            .setMinApi(parameters.getRuntime())
+            .run(parameters.getRuntime(), C.class)
+            .assertSuccessWithOutputLines("1", "2", "A.methodA", "A.methodB", "A.methodC", "B.foo")
+            .inspect(
+                inspector -> {
+                  ClassSubject clazzB = inspector.clazz(B.class);
+                  assertThat(clazzB, isPresent());
+                  assertTrue(clazzB.isRenamed());
+                  ClassSubject clazzA = inspector.clazz(A.class);
+                  assertThat(clazzA, isPresent());
+                  assertEquals("a", clazzA.getFinalName());
+                  FieldSubject fieldA = clazzA.uniqueFieldWithName("fieldA");
+                  assertThat(fieldA, isPresent());
+                  assertEquals("a", fieldA.getFinalName());
+                  MethodSubject methodA = clazzA.uniqueMethodWithName("methodA");
+                  assertThat(methodA, isPresent());
+                  assertEquals("a", methodA.getFinalName());
+                  FieldSubject fieldB = clazzA.uniqueFieldWithName("fieldB");
+                  assertThat(fieldB, isPresent());
+                  assertTrue(fieldB.isRenamed());
+                  MethodSubject methodB = clazzA.uniqueMethodWithName("methodB");
+                  assertThat(methodB, isPresent());
+                  assertTrue(methodB.isRenamed());
+                  MethodSubject methodC = clazzA.uniqueMethodWithName("methodC");
+                  assertThat(methodC, isPresent());
+                  assertFalse(methodC.isRenamed());
+                });
+    // Ensure that the proguard map is extended with all the new minified names.
+    for (String pgLine : pgMap) {
+      runResult.proguardMap().contains(pgLine);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingRotateNameClashTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingRotateNameClashTest.java
index 3526829..ff87272 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingRotateNameClashTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingRotateNameClashTest.java
@@ -45,6 +45,7 @@
         .addLibraryClasses(A.class, B.class)
         .addLibraryFiles(TestBase.runtimeJar(parameters.getBackend()))
         .addProgramClasses(C.class)
+        .addKeepMainRule(C.class)
         .noTreeShaking()
         .addApplyMapping(
             StringUtils.lines(
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/desugar/DefaultInterfaceMethodTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/desugar/DefaultInterfaceMethodTest.java
index 3bcc25c..58cdef6 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/desugar/DefaultInterfaceMethodTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/desugar/DefaultInterfaceMethodTest.java
@@ -99,6 +99,7 @@
         .addProgramClasses(ProgramClass.class)
         .addClasspathClasses(LibraryInterface.class)
         .addApplyMapping(libraryResult.getProguardMap())
+        .addKeepMainRule(ProgramClass.class)
         .setMinApi(parameters.getRuntime())
         .compile()
         .addRunClasspathFiles(libraryResult.writeToZip())