Extend repackaging to allow alpha renaming of classes for testing

Bug: 169036452
Change-Id: I09e162f15c4f6f92b5e7bee0c5bcae35b1160823
diff --git a/src/main/java/com/android/tools/r8/repackaging/Repackaging.java b/src/main/java/com/android/tools/r8/repackaging/Repackaging.java
index 5868aa8..95ee828 100644
--- a/src/main/java/com/android/tools/r8/repackaging/Repackaging.java
+++ b/src/main/java/com/android/tools/r8/repackaging/Repackaging.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.repackaging;
 
 import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
+import static com.android.tools.r8.utils.DescriptorUtils.DESCRIPTOR_PACKAGE_SEPARATOR;
 import static com.android.tools.r8.utils.DescriptorUtils.INNER_CLASS_SEPARATOR;
 
 import com.android.tools.r8.graph.AppView;
@@ -18,7 +19,7 @@
 import com.android.tools.r8.graph.SortedProgramPackageCollection;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.ProguardConfiguration;
-import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
@@ -43,13 +44,14 @@
 public class Repackaging {
 
   private final AppView<AppInfoWithLiveness> appView;
-  private final DexItemFactory dexItemFactory;
   private final ProguardConfiguration proguardConfiguration;
+  private final RepackagingConfiguration repackagingConfiguration;
 
   public Repackaging(AppView<AppInfoWithLiveness> appView) {
     this.appView = appView;
-    this.dexItemFactory = appView.dexItemFactory();
     this.proguardConfiguration = appView.options().getProguardConfiguration();
+    this.repackagingConfiguration =
+        appView.options().testing.repackagingConfigurationFactory.apply(appView);
   }
 
   public RepackagingLens run(
@@ -90,10 +92,16 @@
     Iterator<ProgramPackage> iterator = packages.iterator();
     while (iterator.hasNext()) {
       ProgramPackage pkg = iterator.next();
-      String newPackageDescriptor = getNewPackageDescriptor(pkg, seenPackageDescriptors);
+      String newPackageDescriptor =
+          repackagingConfiguration.getNewPackageDescriptor(pkg, seenPackageDescriptors);
       if (pkg.getPackageDescriptor().equals(newPackageDescriptor)) {
         for (DexProgramClass alreadyRepackagedClass : pkg) {
-          mappings.put(alreadyRepackagedClass.getType(), alreadyRepackagedClass.getType());
+          if (!appView.appInfo().isRepackagingAllowed(alreadyRepackagedClass.getType())) {
+            mappings.put(alreadyRepackagedClass.getType(), alreadyRepackagedClass.getType());
+          }
+        }
+        for (DexProgramClass alreadyRepackagedClass : pkg) {
+          processClass(alreadyRepackagedClass, pkg, newPackageDescriptor, mappings);
         }
         seenPackageDescriptors.add(newPackageDescriptor);
         iterator.remove();
@@ -111,7 +119,8 @@
     // desired package.
     for (ProgramPackage pkg : packages) {
       // Already processed packages should have been removed.
-      String newPackageDescriptor = getNewPackageDescriptor(pkg, seenPackageDescriptors);
+      String newPackageDescriptor =
+          repackagingConfiguration.getNewPackageDescriptor(pkg, seenPackageDescriptors);
       assert !pkg.getPackageDescriptor().equals(newPackageDescriptor);
 
       Iterable<DexProgramClass> classesToRepackage =
@@ -151,7 +160,8 @@
     }
     mappings.put(
         classToRepackage.getType(),
-        getRepackagedType(classToRepackage, outerClass, newPackageDescriptor, mappings));
+        repackagingConfiguration.getRepackagedType(
+            classToRepackage, outerClass, newPackageDescriptor, mappings));
   }
 
   private Iterable<DexProgramClass> computeClassesToRepackage(
@@ -165,46 +175,129 @@
     return constraintGraph.computeClassesToRepackage();
   }
 
-  private String getNewPackageDescriptor(ProgramPackage pkg, Set<String> seenPackageDescriptors) {
-    String newPackageDescriptor =
-        StringUtils.replaceAll(proguardConfiguration.getPackagePrefix(), ".", "/");
-    if (proguardConfiguration.getPackageObfuscationMode().isRepackageClasses()) {
-      return newPackageDescriptor;
-    }
-    if (!newPackageDescriptor.isEmpty()) {
-      newPackageDescriptor += "/";
-    }
-    newPackageDescriptor += pkg.getLastPackageName();
-    String finalPackageDescriptor = newPackageDescriptor;
-    for (int i = 1; seenPackageDescriptors.contains(finalPackageDescriptor); i++) {
-      finalPackageDescriptor = newPackageDescriptor + INNER_CLASS_SEPARATOR + i;
-    }
-    return finalPackageDescriptor;
+  public interface RepackagingConfiguration {
+
+    String getNewPackageDescriptor(ProgramPackage pkg, Set<String> seenPackageDescriptors);
+
+    DexType getRepackagedType(
+        DexProgramClass clazz,
+        DexProgramClass outerClass,
+        String newPackageDescriptor,
+        BiMap<DexType, DexType> mappings);
   }
 
-  private DexType getRepackagedType(
-      DexProgramClass clazz,
-      DexProgramClass outerClass,
-      String newPackageDescriptor,
-      BiMap<DexType, DexType> mappings) {
-    DexType repackagedDexType =
-        clazz.getType().replacePackage(newPackageDescriptor, dexItemFactory);
-    if (outerClass != null) {
-      String simpleName = clazz.getType().getSimpleName();
-      String outerClassSimpleName = outerClass.getType().getSimpleName();
-      if (simpleName.startsWith(outerClassSimpleName + INNER_CLASS_SEPARATOR)) {
-        String newSimpleName =
-            mappings.get(outerClass.getType()).getSimpleName()
-                + simpleName.substring(outerClassSimpleName.length());
-        repackagedDexType = repackagedDexType.withSimpleName(newSimpleName, dexItemFactory);
+  public static class DefaultRepackagingConfiguration implements RepackagingConfiguration {
+
+    private final DexItemFactory dexItemFactory;
+    private final ProguardConfiguration proguardConfiguration;
+
+    public DefaultRepackagingConfiguration(
+        DexItemFactory dexItemFactory, ProguardConfiguration proguardConfiguration) {
+      this.dexItemFactory = dexItemFactory;
+      this.proguardConfiguration = proguardConfiguration;
+    }
+
+    @Override
+    public String getNewPackageDescriptor(ProgramPackage pkg, Set<String> seenPackageDescriptors) {
+      String newPackageDescriptor =
+          DescriptorUtils.getBinaryNameFromJavaType(proguardConfiguration.getPackagePrefix());
+      if (proguardConfiguration.getPackageObfuscationMode().isRepackageClasses()) {
+        return newPackageDescriptor;
       }
+      if (!newPackageDescriptor.isEmpty()) {
+        newPackageDescriptor += DESCRIPTOR_PACKAGE_SEPARATOR;
+      }
+      newPackageDescriptor += pkg.getLastPackageName();
+      String finalPackageDescriptor = newPackageDescriptor;
+      for (int i = 1; seenPackageDescriptors.contains(finalPackageDescriptor); i++) {
+        finalPackageDescriptor = newPackageDescriptor + INNER_CLASS_SEPARATOR + i;
+      }
+      return finalPackageDescriptor;
     }
-    DexType finalRepackagedDexType = repackagedDexType;
-    for (int i = 1; mappings.inverse().containsKey(finalRepackagedDexType); i++) {
-      finalRepackagedDexType =
-          repackagedDexType.addSuffix(
-              Character.toString(INNER_CLASS_SEPARATOR) + i, dexItemFactory);
+
+    @Override
+    public DexType getRepackagedType(
+        DexProgramClass clazz,
+        DexProgramClass outerClass,
+        String newPackageDescriptor,
+        BiMap<DexType, DexType> mappings) {
+      DexType repackagedDexType =
+          clazz.getType().replacePackage(newPackageDescriptor, dexItemFactory);
+      // Rename the class consistently with its outer class.
+      if (outerClass != null) {
+        String simpleName = clazz.getType().getSimpleName();
+        String outerClassSimpleName = outerClass.getType().getSimpleName();
+        if (simpleName.startsWith(outerClassSimpleName + INNER_CLASS_SEPARATOR)) {
+          String newSimpleName =
+              mappings.get(outerClass.getType()).getSimpleName()
+                  + simpleName.substring(outerClassSimpleName.length());
+          repackagedDexType = repackagedDexType.withSimpleName(newSimpleName, dexItemFactory);
+        } else {
+          assert false
+              : "Unexpected name for inner class: "
+                  + clazz.getType().toSourceString()
+                  + " (outer class: "
+                  + outerClass.getType().toSourceString()
+                  + ")";
+        }
+      }
+      // Ensure that the generated name is unique.
+      DexType finalRepackagedDexType = repackagedDexType;
+      for (int i = 1; mappings.inverse().containsKey(finalRepackagedDexType); i++) {
+        finalRepackagedDexType =
+            repackagedDexType.addSuffix(
+                Character.toString(INNER_CLASS_SEPARATOR) + i, dexItemFactory);
+      }
+      return finalRepackagedDexType;
     }
-    return finalRepackagedDexType;
+  }
+
+  /** Testing only. */
+  public static class SuffixRenamingRepackagingConfiguration implements RepackagingConfiguration {
+
+    private final String classNameSuffix;
+    private final DexItemFactory dexItemFactory;
+
+    public SuffixRenamingRepackagingConfiguration(
+        String classNameSuffix, DexItemFactory dexItemFactory) {
+      this.classNameSuffix = classNameSuffix;
+      this.dexItemFactory = dexItemFactory;
+    }
+
+    @Override
+    public String getNewPackageDescriptor(ProgramPackage pkg, Set<String> seenPackageDescriptors) {
+      // Don't change the package of classes.
+      return pkg.getPackageDescriptor();
+    }
+
+    @Override
+    public DexType getRepackagedType(
+        DexProgramClass clazz,
+        DexProgramClass outerClass,
+        String newPackageDescriptor,
+        BiMap<DexType, DexType> mappings) {
+      DexType repackagedDexType = clazz.getType();
+      // Rename the class consistently with its outer class.
+      if (outerClass != null) {
+        String simpleName = clazz.getType().getSimpleName();
+        String outerClassSimpleName = outerClass.getType().getSimpleName();
+        if (simpleName.startsWith(outerClassSimpleName + INNER_CLASS_SEPARATOR)) {
+          String newSimpleName =
+              mappings.get(outerClass.getType()).getSimpleName()
+                  + simpleName.substring(outerClassSimpleName.length());
+          repackagedDexType = repackagedDexType.withSimpleName(newSimpleName, dexItemFactory);
+        }
+      }
+      // Append the class name suffix to all classes.
+      repackagedDexType = repackagedDexType.addSuffix(classNameSuffix, dexItemFactory);
+      // Ensure that the generated name is unique.
+      DexType finalRepackagedDexType = repackagedDexType;
+      for (int i = 1; mappings.inverse().containsKey(finalRepackagedDexType); i++) {
+        finalRepackagedDexType =
+            repackagedDexType.addSuffix(
+                Character.toString(INNER_CLASS_SEPARATOR) + i, dexItemFactory);
+      }
+      return finalRepackagedDexType;
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 15183a1..af11559 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -43,6 +43,8 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.Position;
 import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.repackaging.Repackaging.DefaultRepackagingConfiguration;
+import com.android.tools.r8.repackaging.Repackaging.RepackagingConfiguration;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.Enqueuer;
 import com.android.tools.r8.shaking.GlobalKeepInfoConfiguration;
@@ -75,6 +77,7 @@
 import java.util.function.BiConsumer;
 import java.util.function.BiPredicate;
 import java.util.function.Consumer;
+import java.util.function.Function;
 import java.util.function.Predicate;
 import org.objectweb.asm.Opcodes;
 
@@ -1213,6 +1216,12 @@
 
     public BiConsumer<ProgramMethod, MethodProcessingId> methodProcessingIdConsumer = null;
 
+    public Function<AppView<AppInfoWithLiveness>, RepackagingConfiguration>
+        repackagingConfigurationFactory =
+            appView ->
+                new DefaultRepackagingConfiguration(
+                    appView.dexItemFactory(), appView.options().getProguardConfiguration());
+
     public Consumer<Deque<SortedProgramMethodSet>> waveModifier = waves -> {};
 
     /**
diff --git a/src/test/java/com/android/tools/r8/repackage/RepackageWithSuffixRenamingConfigurationTest.java b/src/test/java/com/android/tools/r8/repackage/RepackageWithSuffixRenamingConfigurationTest.java
new file mode 100644
index 0000000..b22042f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/RepackageWithSuffixRenamingConfigurationTest.java
@@ -0,0 +1,78 @@
+// Copyright (c) 2020, 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.repackage;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.repackaging.Repackaging.SuffixRenamingRepackagingConfiguration;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class RepackageWithSuffixRenamingConfigurationTest extends RepackageTestBase {
+
+  public RepackageWithSuffixRenamingConfigurationTest(
+      String flattenPackageHierarchyOrRepackageClasses, TestParameters parameters) {
+    super(flattenPackageHierarchyOrRepackageClasses, parameters);
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .addKeepClassRules(GreeterFoo.class)
+        .addOptionsModification(
+            options ->
+                options.testing.repackagingConfigurationFactory =
+                    appView ->
+                        new SuffixRenamingRepackagingConfiguration("Foo", appView.dexItemFactory()))
+        .apply(this::configureRepackaging)
+        .enableInliningAnnotations()
+        .noMinification()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("Hello world!");
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject greeterSubject = inspector.clazz(Greeter.class);
+    assertEquals(GreeterFoo.class.getTypeName() + "$1", greeterSubject.getFinalName());
+
+    ClassSubject greeterFooSubject = inspector.clazz(GreeterFoo.class);
+    assertEquals(GreeterFoo.class.getTypeName(), greeterFooSubject.getFinalName());
+  }
+
+  public static class TestClass {
+
+    public static void main(String[] args) {
+      Greeter.greet();
+      GreeterFoo.greet();
+    }
+  }
+
+  public static class Greeter extends Exception {
+
+    @NeverInline
+    public static void greet() {
+      System.out.print("Hello");
+    }
+  }
+
+  public static class GreeterFoo extends Exception {
+
+    @NeverInline
+    public static void greet() {
+      System.out.println(" world!");
+    }
+  }
+}