Enable -dontusemixedcaseclassnames and allow numbers in identifiers

Bug: 129493704
Bug: 132812927
Change-Id: Id32cf84007abb4f6067f3fa44d96e0c97edc87af
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java
index 166ec88..a3e71e6 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java
@@ -21,7 +21,8 @@
 import com.android.tools.r8.ir.optimize.MemberPoolCollection.MemberPool;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.MethodSignatureEquivalence;
-import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.SymbolGenerationUtils;
+import com.android.tools.r8.utils.SymbolGenerationUtils.MixedCasing;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.base.Equivalence.Wrapper;
@@ -148,8 +149,10 @@
               appView
                   .dexItemFactory()
                   .createString(
-                      StringUtils.numberToIdentifier(
-                          count, method.method.name.toSourceString().toCharArray()));
+                      SymbolGenerationUtils.numberToIdentifier(
+                          count,
+                          MixedCasing.USE_MIXED_CASE,
+                          method.method.name.toSourceString().toCharArray()));
         } else {
           // Constructors must be named `<init>`.
           return null;
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 db8a84b..adbc1c3 100644
--- a/src/main/java/com/android/tools/r8/naming/MethodNamingState.java
+++ b/src/main/java/com/android/tools/r8/naming/MethodNamingState.java
@@ -7,7 +7,6 @@
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.utils.StringUtils;
 import java.io.PrintStream;
 import java.util.HashMap;
 import java.util.Map;
@@ -340,20 +339,11 @@
 
     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();
+      out.print("public name count: ");
+      out.print(virtualNameCount);
+      out.print(", ");
+      out.print("direct name count: ");
+      out.println(directNameCount);
     }
   }
 }
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 b86cfc4..6906a06 100644
--- a/src/main/java/com/android/tools/r8/naming/Minifier.java
+++ b/src/main/java/com/android/tools/r8/naming/Minifier.java
@@ -22,7 +22,8 @@
 import com.android.tools.r8.naming.FieldNameMinifier.FieldRenaming;
 import com.android.tools.r8.naming.MethodNameMinifier.MethodRenaming;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.SymbolGenerationUtils;
+import com.android.tools.r8.utils.SymbolGenerationUtils.MixedCasing;
 import com.android.tools.r8.utils.Timing;
 import java.util.HashSet;
 import java.util.List;
@@ -90,10 +91,13 @@
     // dictionary. We use a list for direct indexing based on a number and a set for looking up.
     private final List<String> obfuscationDictionary;
     private final Set<String> obfuscationDictionaryForLookup;
+    private final MixedCasing mixedCasing;
 
-    BaseMinificationNamingStrategy(List<String> obfuscationDictionary) {
+    BaseMinificationNamingStrategy(List<String> obfuscationDictionary, boolean dontUseMixedCasing) {
       this.obfuscationDictionary = obfuscationDictionary;
       this.obfuscationDictionaryForLookup = new HashSet<>(this.obfuscationDictionary);
+      this.mixedCasing =
+          dontUseMixedCasing ? MixedCasing.DONT_USE_MIXED_CASE : MixedCasing.USE_MIXED_CASE;
       assert obfuscationDictionary != null;
     }
 
@@ -105,7 +109,9 @@
       } else {
         String nextString;
         do {
-          nextString = StringUtils.numberToIdentifier(state.incrementNameIndex(isDirectMethodCall));
+          nextString =
+              SymbolGenerationUtils.numberToIdentifier(
+                  state.incrementNameIndex(isDirectMethodCall), mixedCasing);
         } while (obfuscationDictionaryForLookup.contains(nextString));
         nextName.append(nextString);
       }
@@ -120,7 +126,9 @@
     private final DexItemFactory factory;
 
     MinificationClassNamingStrategy(AppView<?> appView) {
-      super(appView.options().getProguardConfiguration().getClassObfuscationDictionary());
+      super(
+          appView.options().getProguardConfiguration().getClassObfuscationDictionary(),
+          appView.options().getProguardConfiguration().hasDontUseMixedCaseClassnames());
       this.appView = appView;
       factory = appView.dexItemFactory();
     }
@@ -140,7 +148,9 @@
       implements PackageNamingStrategy {
 
     MinificationPackageNamingStrategy(AppView<?> appView) {
-      super(appView.options().getProguardConfiguration().getPackageObfuscationDictionary());
+      super(
+          appView.options().getProguardConfiguration().getPackageObfuscationDictionary(),
+          appView.options().getProguardConfiguration().hasDontUseMixedCaseClassnames());
     }
 
     @Override
@@ -160,7 +170,7 @@
     private final DexItemFactory factory;
 
     public MinifierMemberNamingStrategy(AppView<?> appView) {
-      super(appView.options().getProguardConfiguration().getObfuscationDictionary());
+      super(appView.options().getProguardConfiguration().getObfuscationDictionary(), false);
       this.appView = appView;
       this.factory = appView.dexItemFactory();
     }
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
index d46a976..9d0adec 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
@@ -69,6 +69,7 @@
     private boolean overloadAggressively;
     private boolean keepRuleSynthesisForRecompilation = false;
     private boolean configurationDebugging = false;
+    private boolean dontUseMixedCaseClassnames = false;
 
     private Builder(DexItemFactory dexItemFactory, Reporter reporter) {
       this.dexItemFactory = dexItemFactory;
@@ -275,6 +276,10 @@
       this.configurationDebugging = configurationDebugging;
     }
 
+    public void setDontUseMixedCaseClassnames(boolean dontUseMixedCaseClassnames) {
+      this.dontUseMixedCaseClassnames = dontUseMixedCaseClassnames;
+    }
+
     /**
      * This synthesizes a set of keep rules that are necessary in order to be able to successfully
      * recompile the generated dex files with the same keep rules.
@@ -332,7 +337,8 @@
               adaptResourceFilenames.build(),
               adaptResourceFileContents.build(),
               keepDirectories.build(),
-              configurationDebugging);
+              configurationDebugging,
+              dontUseMixedCaseClassnames);
 
       reporter.failIfPendingErrors();
 
@@ -400,6 +406,7 @@
   private final ProguardPathFilter adaptResourceFileContents;
   private final ProguardPathFilter keepDirectories;
   private final boolean configurationDebugging;
+  private final boolean dontUseMixedCaseClassnames;
 
   private ProguardConfiguration(
       String parsedConfiguration,
@@ -438,7 +445,8 @@
       ProguardPathFilter adaptResourceFilenames,
       ProguardPathFilter adaptResourceFileContents,
       ProguardPathFilter keepDirectories,
-      boolean configurationDebugging) {
+      boolean configurationDebugging,
+      boolean dontUseMixedCaseClassnames) {
     this.parsedConfiguration = parsedConfiguration;
     this.dexItemFactory = factory;
     this.injars = ImmutableList.copyOf(injars);
@@ -476,6 +484,7 @@
     this.adaptResourceFileContents = adaptResourceFileContents;
     this.keepDirectories = keepDirectories;
     this.configurationDebugging = configurationDebugging;
+    this.dontUseMixedCaseClassnames = dontUseMixedCaseClassnames;
   }
 
   /**
@@ -638,6 +647,10 @@
     return configurationDebugging;
   }
 
+  public boolean hasDontUseMixedCaseClassnames() {
+    return dontUseMixedCaseClassnames;
+  }
+
   @Override
   public String toString() {
     StringBuilder builder = new StringBuilder();
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
index e4113b3..ae25876 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -62,7 +62,6 @@
 
   private static final List<String> IGNORED_FLAG_OPTIONS = ImmutableList.of(
       "forceprocessing",
-      "dontusemixedcaseclassnames",
       "dontpreverify",
       "experimentalshrinkunusedprotofields",
       "filterlibraryjarswithorginalprogramjars",
@@ -401,6 +400,8 @@
         configurationBuilder.addRule(parseIfRule(optionStart));
       } else if (acceptString("addconfigurationdebugging")) {
         configurationBuilder.setConfigurationDebugging(true);
+      } else if (acceptString("dontusemixedcaseclassnames")) {
+        configurationBuilder.setDontUseMixedCaseClassnames(true);
       } else {
         String unknownOption = acceptString();
         String devMessage = "";
diff --git a/src/main/java/com/android/tools/r8/utils/StringUtils.java b/src/main/java/com/android/tools/r8/utils/StringUtils.java
index 9064579..05900cb 100644
--- a/src/main/java/com/android/tools/r8/utils/StringUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/StringUtils.java
@@ -17,10 +17,6 @@
   public static final String[] EMPTY_ARRAY = {};
   public static final String LINE_SEPARATOR = System.getProperty("line.separator");
 
-  private static final char[] IDENTIFIER_LETTERS
-      = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_".toCharArray();
-  private static final int NUMBER_OF_LETTERS = IDENTIFIER_LETTERS.length;
-
   public enum BraceType {
     PARENS,
     SQUARE,
@@ -257,39 +253,6 @@
     return Arrays.toString(digest);
   }
 
-  public static String numberToIdentifier(int nameCount) {
-    return numberToIdentifier(nameCount, EMPTY_CHAR_ARRAY, false);
-  }
-
-  public static String numberToIdentifier(int nameCount, char[] prefix) {
-    return numberToIdentifier(nameCount, prefix, false);
-  }
-
-  public static String numberToIdentifier(int nameCount, char[] prefix, boolean addSemicolon) {
-    // TODO(b132812927): Add support for using numbers.
-    int size = addSemicolon ? 1 : 0;
-    int number = nameCount;
-    while (number >= NUMBER_OF_LETTERS) {
-      number /= NUMBER_OF_LETTERS;
-      size++;
-    }
-    size++;
-    char characters[] = Arrays.copyOfRange(prefix, 0, prefix.length + size);
-    number = nameCount;
-
-    int i = prefix.length;
-    while (number >= NUMBER_OF_LETTERS) {
-      characters[i++] = IDENTIFIER_LETTERS[number % NUMBER_OF_LETTERS];
-      number /= NUMBER_OF_LETTERS;
-    }
-    characters[i++] = IDENTIFIER_LETTERS[number - 1];
-    if (addSemicolon) {
-      characters[i++] = ';';
-    }
-    assert i == characters.length;
-
-    return new String(characters);
-  }
 
   public static String times(String string, int count) {
     StringBuilder builder = new StringBuilder();
diff --git a/src/main/java/com/android/tools/r8/utils/SymbolGenerationUtils.java b/src/main/java/com/android/tools/r8/utils/SymbolGenerationUtils.java
new file mode 100644
index 0000000..b66ca5d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/SymbolGenerationUtils.java
@@ -0,0 +1,73 @@
+// 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.utils;
+
+import static com.android.tools.r8.utils.StringUtils.EMPTY_CHAR_ARRAY;
+
+import java.util.Arrays;
+
+public class SymbolGenerationUtils {
+
+  public enum MixedCasing {
+    USE_MIXED_CASE,
+    DONT_USE_MIXED_CASE
+  }
+
+  // These letters are used not creating fresh names to output and not for parsing dex/class files.
+  private static final char[] IDENTIFIER_CHARACTERS =
+      "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
+  private static final int NUMBER_OF_CHARACTERS = IDENTIFIER_CHARACTERS.length;
+  private static final int NUMBER_OF_CHARACTERS_MINUS_CAPITAL_LETTERS = NUMBER_OF_CHARACTERS - 26;
+  private static final int NON_ALLOWED_FIRST_CHARACTERS = 10;
+
+  public static String numberToIdentifier(int nameCount, MixedCasing mixedCasing) {
+    return numberToIdentifier(nameCount, mixedCasing, EMPTY_CHAR_ARRAY, false);
+  }
+
+  public static String numberToIdentifier(int nameCount, MixedCasing mixedCasing, char[] prefix) {
+    return numberToIdentifier(nameCount, mixedCasing, prefix, false);
+  }
+
+  public static String numberToIdentifier(
+      int nameCount, MixedCasing mixedCasing, char[] prefix, boolean addSemicolon) {
+    int size = 1;
+    int number = nameCount;
+    int maximumNumberOfCharacters =
+        mixedCasing == MixedCasing.USE_MIXED_CASE
+            ? NUMBER_OF_CHARACTERS
+            : NUMBER_OF_CHARACTERS_MINUS_CAPITAL_LETTERS;
+    int firstNumberOfCharacters = maximumNumberOfCharacters - NON_ALLOWED_FIRST_CHARACTERS;
+    int availableCharacters = firstNumberOfCharacters;
+    // We first do an initial computation to find the size of the resulting string to allocate an
+    // array that will fit in length.
+    while (number > availableCharacters) {
+      number = (number - 1) / availableCharacters;
+      availableCharacters = maximumNumberOfCharacters;
+      size++;
+    }
+    size += addSemicolon ? 1 : 0;
+    char characters[] = Arrays.copyOfRange(prefix, 0, prefix.length + size);
+    number = nameCount;
+
+    int i = prefix.length;
+    availableCharacters = firstNumberOfCharacters;
+    int firstLetterPadding = NON_ALLOWED_FIRST_CHARACTERS;
+    while (number > availableCharacters) {
+      characters[i++] =
+          IDENTIFIER_CHARACTERS[(number - 1) % availableCharacters + firstLetterPadding];
+      number = (number - 1) / availableCharacters;
+      availableCharacters = maximumNumberOfCharacters;
+      firstLetterPadding = 0;
+    }
+    characters[i++] = IDENTIFIER_CHARACTERS[number - 1 + firstLetterPadding];
+    if (addSemicolon) {
+      characters[i++] = ';';
+    }
+    assert i == characters.length;
+    assert !Character.isDigit(characters[prefix.length]);
+
+    return new String(characters);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/MinificationMixedCaseAndNumbersTest.java b/src/test/java/com/android/tools/r8/naming/MinificationMixedCaseAndNumbersTest.java
new file mode 100644
index 0000000..55b3a9a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/MinificationMixedCaseAndNumbersTest.java
@@ -0,0 +1,204 @@
+// 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.SymbolGenerationUtils;
+import com.android.tools.r8.utils.SymbolGenerationUtils.MixedCasing;
+import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class MinificationMixedCaseAndNumbersTest extends TestBase {
+
+  private static final int NUMBER_OF_MINIFIED_CLASSES = 60;
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().build();
+  }
+
+  public MinificationMixedCaseAndNumbersTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testNaming() throws ExecutionException, CompilationFailedException, IOException {
+    Set<String> allowedNames = new HashSet<>();
+    allowedNames.add("com.android.tools.r8.naming.MinificationMixedCaseAndNumbersTest$Main");
+    for (int i = 1; i < NUMBER_OF_MINIFIED_CLASSES; i++) {
+      String newString =
+          SymbolGenerationUtils.numberToIdentifier(i, MixedCasing.DONT_USE_MIXED_CASE);
+      assertFalse(Character.isDigit(newString.charAt(0)));
+      allowedNames.add("com.android.tools.r8.naming." + newString);
+    }
+    testForR8(parameters.getBackend())
+        .addInnerClasses(MinificationMixedCaseAndNumbersTest.class)
+        .addKeepMainRule(Main.class)
+        .noTreeShaking()
+        .addKeepRules("-dontusemixedcaseclassnames")
+        .setMinApi(parameters.getRuntime())
+        .run(parameters.getRuntime(), Main.class)
+        .inspect(
+            inspector -> {
+              List<FoundClassSubject> foundClassSubjects = new ArrayList<>(inspector.allClasses());
+              foundClassSubjects.forEach(
+                  foundClazz -> {
+                    assertTrue(allowedNames.contains(foundClazz.getFinalName()));
+                    allowedNames.remove(foundClazz.getFinalName());
+                  });
+            });
+    // The first identifier to use a number is 27 for MixedCasing.DONT_USE_MIXED_CASE.
+    assertTrue(allowedNames.isEmpty());
+    assertEquals(
+        "a0", SymbolGenerationUtils.numberToIdentifier(27, MixedCasing.DONT_USE_MIXED_CASE));
+    assertEquals(
+        "zz", SymbolGenerationUtils.numberToIdentifier(962, MixedCasing.DONT_USE_MIXED_CASE));
+    assertEquals(
+        "a00", SymbolGenerationUtils.numberToIdentifier(963, MixedCasing.DONT_USE_MIXED_CASE));
+  }
+
+  public static class A {}
+
+  public static class B {}
+
+  public static class C {}
+
+  public static class D {}
+
+  public static class E {}
+
+  public static class F {}
+
+  public static class G {}
+
+  public static class H {}
+
+  public static class I {}
+
+  public static class J {}
+
+  public static class K {}
+
+  public static class L {}
+
+  public static class M {}
+
+  public static class N {}
+
+  public static class O {}
+
+  public static class P {}
+
+  public static class Q {}
+
+  public static class R {}
+
+  public static class S {}
+
+  public static class T {}
+
+  public static class U {}
+
+  public static class V {}
+
+  public static class W {}
+
+  public static class X {}
+
+  public static class Y {}
+
+  public static class Z {}
+
+  public static class a {}
+
+  public static class b {}
+
+  public static class c {}
+
+  public static class d {}
+
+  public static class e {}
+
+  public static class f {}
+
+  public static class g {}
+
+  public static class h {}
+
+  public static class i {}
+
+  public static class j {}
+
+  public static class k {}
+
+  public static class l {}
+
+  public static class m {}
+
+  public static class n {}
+
+  public static class o {}
+
+  public static class p {}
+
+  public static class q {}
+
+  public static class r {}
+
+  public static class s {}
+
+  public static class t {}
+
+  public static class u {}
+
+  public static class v {}
+
+  public static class w {}
+
+  public static class x {}
+
+  public static class y {}
+
+  public static class z {}
+
+  public static class AA {}
+
+  public static class AB {}
+
+  public static class AC {}
+
+  public static class AD {}
+
+  public static class AE {}
+
+  public static class AF {}
+
+  public static class AG {}
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      System.out.println("HELLO WORLD");
+    }
+  }
+}