Add tests for class and package obfuscation dictionary

This CL also rewrites the class and package strategy to centralize the
logic regarding naming.

Bug: 132666215
Change-Id: I92b97e7149114f92b17a00261329b905efeddaf0
diff --git a/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java b/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
index 1b50601..10430cc 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
@@ -30,8 +30,6 @@
 import com.google.common.collect.Sets;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
@@ -50,8 +48,6 @@
 
   private final Map<DexType, DexString> renaming = Maps.newIdentityHashMap();
   private final Map<String, Namespace> states = new HashMap<>();
-  private final List<String> packageDictionary;
-  private final List<String> classDictionary;
   private final boolean keepInnerClassStructure;
 
   private final Namespace topLevelState;
@@ -69,8 +65,6 @@
     this.packageObfuscationMode = options.getProguardConfiguration().getPackageObfuscationMode();
     this.isAccessModificationAllowed =
         options.getProguardConfiguration().isAccessModificationAllowed();
-    this.packageDictionary = options.getProguardConfiguration().getPackageObfuscationDictionary();
-    this.classDictionary = options.getProguardConfiguration().getClassObfuscationDictionary();
     this.keepInnerClassStructure =
         options.getProguardConfiguration().getKeepAttributes().signature
             || options.getProguardConfiguration().getKeepAttributes().innerClasses;
@@ -356,12 +350,12 @@
     }
   }
 
-  protected class Namespace {
+  protected class Namespace implements InternalNamingState {
 
     private final String packageName;
     private final char[] packagePrefix;
-    private final Iterator<String> packageDictionaryIterator;
-    private final Iterator<String> classDictionaryIterator;
+    private int dictionaryIndex = 0;
+    private int nameIndex = 1;
 
     Namespace(String packageName) {
       this(packageName, String.valueOf(DESCRIPTOR_PACKAGE_SEPARATOR));
@@ -373,8 +367,6 @@
           // L or La/b/ (or La/b/C$)
           + (packageName.isEmpty() ? "" : separator))
           .toCharArray();
-      this.packageDictionaryIterator = packageDictionary.iterator();
-      this.classDictionaryIterator = classDictionary.iterator();
 
       // R.class in Android, which contains constant IDs to assets, can be bundled at any time.
       // Insert `R` immediately so that the class name minifier can skip that name by default.
@@ -386,63 +378,49 @@
       return packageName;
     }
 
-    private DexString nextSuggestedNameForClass(DexType type) {
-      StringBuilder nextName = new StringBuilder();
-      if (!classNamingStrategy.bypassDictionary() && classDictionaryIterator.hasNext()) {
-        nextName.append(packagePrefix).append(classDictionaryIterator.next()).append(';');
-        return appView.dexItemFactory().createString(nextName.toString());
-      } else {
-        return classNamingStrategy.next(this, type, packagePrefix);
-      }
-    }
-
     DexString nextTypeName(DexType type) {
       DexString candidate;
       do {
-        candidate = nextSuggestedNameForClass(type);
+        candidate = classNamingStrategy.next(type, packagePrefix, this);
       } while (usedTypeNames.contains(candidate));
       usedTypeNames.add(candidate);
       return candidate;
     }
 
-    private String nextSuggestedNameForSubpackage() {
-      // Note that the differences between this method and the other variant for class renaming are
-      // 1) this one uses the different dictionary and counter,
-      // 2) this one does not append ';' at the end, and
-      // 3) this one removes 'L' at the beginning to make the return value a binary form.
-      if (!packageNamingStrategy.bypassDictionary() && packageDictionaryIterator.hasNext()) {
-        StringBuilder nextName = new StringBuilder();
-        nextName.append(packagePrefix).append(packageDictionaryIterator.next());
-        return nextName.toString().substring(1);
-      } else {
-        return packageNamingStrategy.next(this, packagePrefix);
-      }
-    }
-
     String nextPackagePrefix() {
       String candidate;
       do {
-        candidate = nextSuggestedNameForSubpackage();
+        candidate = packageNamingStrategy.next(packagePrefix, this);
       } while (usedPackagePrefixes.contains(candidate));
       usedPackagePrefixes.add(candidate);
       return candidate;
     }
+
+    @Override
+    public int getDictionaryIndex() {
+      return dictionaryIndex;
+    }
+
+    @Override
+    public int incrementDictionaryIndex() {
+      return dictionaryIndex++;
+    }
+
+    @Override
+    public int incrementNameIndex(boolean isDirectMethodCall) {
+      assert !isDirectMethodCall;
+      return nameIndex++;
+    }
   }
 
   protected interface ClassNamingStrategy {
-
-    DexString next(Namespace namespace, DexType type, char[] packagePrefix);
-
-    boolean bypassDictionary();
+    DexString next(DexType type, char[] packagePrefix, InternalNamingState state);
 
     boolean noObfuscation(DexType type);
   }
 
   protected interface PackageNamingStrategy {
-
-    String next(Namespace namespace, char[] packagePrefix);
-
-    boolean bypassDictionary();
+    String next(char[] packagePrefix, InternalNamingState state);
   }
 
   /**
diff --git a/src/main/java/com/android/tools/r8/naming/FieldNamingState.java b/src/main/java/com/android/tools/r8/naming/FieldNamingState.java
index b713582..d654558 100644
--- a/src/main/java/com/android/tools/r8/naming/FieldNamingState.java
+++ b/src/main/java/com/android/tools/r8/naming/FieldNamingState.java
@@ -12,7 +12,6 @@
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.naming.FieldNamingState.InternalState;
-import com.android.tools.r8.naming.MemberNamingStrategy.MemberNamingInternalState;
 import java.util.IdentityHashMap;
 import java.util.Map;
 
@@ -80,7 +79,7 @@
     return new FieldNamingState(appView, strategy, reservedNames, internalStatesClone);
   }
 
-  class InternalState implements MemberNamingInternalState, Cloneable {
+  class InternalState implements InternalNamingState, Cloneable {
 
     private int dictionaryIndex;
     private int nextNameIndex;
diff --git a/src/main/java/com/android/tools/r8/naming/InternalNamingState.java b/src/main/java/com/android/tools/r8/naming/InternalNamingState.java
new file mode 100644
index 0000000..e5749c1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/naming/InternalNamingState.java
@@ -0,0 +1,14 @@
+// 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;
+
+interface InternalNamingState {
+
+  int getDictionaryIndex();
+
+  int incrementDictionaryIndex();
+
+  int incrementNameIndex(boolean isDirectMethodCall);
+}
diff --git a/src/main/java/com/android/tools/r8/naming/MemberNamingStrategy.java b/src/main/java/com/android/tools/r8/naming/MemberNamingStrategy.java
index 260b176..f9e8ed5 100644
--- a/src/main/java/com/android/tools/r8/naming/MemberNamingStrategy.java
+++ b/src/main/java/com/android/tools/r8/naming/MemberNamingStrategy.java
@@ -11,20 +11,11 @@
 
 public interface MemberNamingStrategy {
 
-  DexString next(DexMethod method, MemberNamingInternalState internalState);
+  DexString next(DexMethod method, InternalNamingState internalState);
 
-  DexString next(DexField field, MemberNamingInternalState internalState);
+  DexString next(DexField field, InternalNamingState internalState);
 
   boolean breakOnNotAvailable(DexReference source, DexString name);
 
   boolean noObfuscation(DexReference reference);
-
-  interface MemberNamingInternalState {
-
-    int getDictionaryIndex();
-
-    int incrementDictionaryIndex();
-
-    int incrementNameIndex(boolean isDirectMethodCall);
-  }
 }
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 7b0c5fb..8b9d042 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.naming.MemberNamingStrategy.MemberNamingInternalState;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.Sets;
 import java.io.PrintStream;
@@ -131,7 +130,7 @@
     }
   }
 
-  class InternalState implements MemberNamingInternalState {
+  class InternalState implements InternalNamingState {
 
     private static final int INITIAL_NAME_COUNT = 1;
     private static final int INITIAL_DICTIONARY_INDEX = 0;
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 4bdf87c..d2424fe 100644
--- a/src/main/java/com/android/tools/r8/naming/Minifier.java
+++ b/src/main/java/com/android/tools/r8/naming/Minifier.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.naming;
 
+import static com.android.tools.r8.utils.StringUtils.EMPTY_CHAR_ARRAY;
+
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexCallSite;
 import com.android.tools.r8.graph.DexClass;
@@ -15,15 +17,12 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.naming.ClassNameMinifier.ClassNamingStrategy;
 import com.android.tools.r8.naming.ClassNameMinifier.ClassRenaming;
-import com.android.tools.r8.naming.ClassNameMinifier.Namespace;
 import com.android.tools.r8.naming.ClassNameMinifier.PackageNamingStrategy;
 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.Timing;
-import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap;
-import it.unimi.dsi.fastutil.objects.Object2IntMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -50,7 +49,7 @@
         new ClassNameMinifier(
             appView,
             new MinificationClassNamingStrategy(appView),
-            new MinificationPackageNamingStrategy(),
+            new MinificationPackageNamingStrategy(appView),
             // Use deterministic class order to make sure renaming is deterministic.
             appView.appInfo().classesWithDeterministicOrder());
     ClassRenaming classRenaming = classNameMinifier.computeRenaming(timing);
@@ -84,29 +83,50 @@
     return lens;
   }
 
-  static class MinificationClassNamingStrategy implements ClassNamingStrategy {
+  abstract static class BaseMinificationNamingStrategy {
+
+    // We have to ensure that the names proposed by the minifier is not used in the obfuscation
+    // 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;
+
+    BaseMinificationNamingStrategy(List<String> obfuscationDictionary) {
+      this.obfuscationDictionary = obfuscationDictionary;
+      this.obfuscationDictionaryForLookup = new HashSet<>(this.obfuscationDictionary);
+      assert obfuscationDictionary != null;
+    }
+
+    String nextName(char[] packagePrefix, InternalNamingState state, boolean isDirectMethodCall) {
+      StringBuilder nextName = new StringBuilder();
+      nextName.append(packagePrefix);
+      if (state.getDictionaryIndex() < obfuscationDictionary.size()) {
+        nextName.append(obfuscationDictionary.get(state.incrementDictionaryIndex()));
+      } else {
+        String nextString;
+        do {
+          nextString = StringUtils.numberToIdentifier(state.incrementNameIndex(isDirectMethodCall));
+        } while (obfuscationDictionaryForLookup.contains(nextString));
+        nextName.append(nextString);
+      }
+      return nextName.toString();
+    }
+  }
+
+  static class MinificationClassNamingStrategy extends BaseMinificationNamingStrategy
+      implements ClassNamingStrategy {
 
     private final AppView<?> appView;
-    private final Object2IntMap<Namespace> namespaceCounters = new Object2IntLinkedOpenHashMap<>();
+    private final DexItemFactory factory;
 
     MinificationClassNamingStrategy(AppView<?> appView) {
+      super(appView.options().getProguardConfiguration().getClassObfuscationDictionary());
       this.appView = appView;
-      namespaceCounters.defaultReturnValue(1);
+      factory = appView.dexItemFactory();
     }
 
     @Override
-    public DexString next(Namespace namespace, DexType type, char[] packagePrefix) {
-      int counter = namespaceCounters.put(namespace, namespaceCounters.getInt(namespace) + 1);
-      DexString string =
-          appView
-              .dexItemFactory()
-              .createString(StringUtils.numberToIdentifier(counter, packagePrefix, true));
-      return string;
-    }
-
-    @Override
-    public boolean bypassDictionary() {
-      return false;
+    public DexString next(DexType type, char[] packagePrefix, InternalNamingState state) {
+      return factory.createString(nextName(packagePrefix, state, false) + ";");
     }
 
     @Override
@@ -115,74 +135,50 @@
     }
   }
 
-  static class MinificationPackageNamingStrategy implements PackageNamingStrategy {
+  static class MinificationPackageNamingStrategy extends BaseMinificationNamingStrategy
+      implements PackageNamingStrategy {
 
-    private final Object2IntMap<Namespace> namespaceCounters = new Object2IntLinkedOpenHashMap<>();
-
-    public MinificationPackageNamingStrategy() {
-      namespaceCounters.defaultReturnValue(1);
+    MinificationPackageNamingStrategy(AppView<?> appView) {
+      super(appView.options().getProguardConfiguration().getPackageObfuscationDictionary());
     }
 
     @Override
-    public String next(Namespace namespace, char[] packagePrefix) {
+    public String next(char[] packagePrefix, InternalNamingState state) {
       // Note that the differences between this method and the other variant for class renaming are
       // 1) this one uses the different dictionary and counter,
       // 2) this one does not append ';' at the end, and
       // 3) this one removes 'L' at the beginning to make the return value a binary form.
-      int counter = namespaceCounters.put(namespace, namespaceCounters.getInt(namespace) + 1);
-      return StringUtils.numberToIdentifier(counter, packagePrefix, false).substring(1);
-    }
-
-    @Override
-    public boolean bypassDictionary() {
-      return false;
+      return nextName(packagePrefix, state, false).substring(1);
     }
   }
 
-  static class MinifierMemberNamingStrategy implements MemberNamingStrategy {
+  static class MinifierMemberNamingStrategy extends BaseMinificationNamingStrategy
+      implements MemberNamingStrategy {
 
     private final DexItemFactory factory;
-    // We have to ensure that the names proposed by the minifier is not used in the obfuscation
-    // 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 AppView<?> appView;
 
     public MinifierMemberNamingStrategy(AppView<?> appView) {
+      super(appView.options().getProguardConfiguration().getObfuscationDictionary());
       this.appView = appView;
       this.factory = appView.dexItemFactory();
-      this.obfuscationDictionary =
-          appView.options().getProguardConfiguration().getObfuscationDictionary();
-      this.obfuscationDictionaryForLookup = new HashSet<>(this.obfuscationDictionary);
-      assert this.obfuscationDictionary != null;
     }
 
     @Override
-    public DexString next(DexMethod method, MemberNamingInternalState internalState) {
+    public DexString next(DexMethod method, InternalNamingState internalState) {
       DexEncodedMethod encodedMethod = appView.definitionFor(method);
       boolean isDirectOrStatic = encodedMethod.isDirectMethod() || encodedMethod.isStatic();
       return getNextName(internalState, isDirectOrStatic);
     }
 
     @Override
-    public DexString next(DexField field, MemberNamingInternalState internalState) {
+    public DexString next(DexField field, InternalNamingState internalState) {
       return getNextName(internalState, false);
     }
 
-    private DexString getNextName(
-        MemberNamingInternalState internalState, boolean isDirectOrStatic) {
-      if (internalState.getDictionaryIndex() < obfuscationDictionary.size()) {
-        return factory.createString(
-            obfuscationDictionary.get(internalState.incrementDictionaryIndex()));
-      } else {
-        String nextString;
-        do {
-          int counter = internalState.incrementNameIndex(isDirectOrStatic);
-          nextString = StringUtils.numberToIdentifier(counter);
-        } while (obfuscationDictionaryForLookup.contains(nextString));
-        return factory.createString(nextString);
-      }
+    private DexString getNextName(InternalNamingState internalState, boolean isDirectOrStatic) {
+      return factory.createString(nextName(EMPTY_CHAR_ARRAY, internalState, isDirectOrStatic));
     }
 
     @Override
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 4c2c8cf..90a6760 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapMinifier.java
@@ -16,7 +16,6 @@
 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.ClassNameMinifier.Namespace;
 import com.android.tools.r8.naming.FieldNameMinifier.FieldRenaming;
 import com.android.tools.r8.naming.MemberNaming.FieldSignature;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
@@ -122,7 +121,7 @@
             // 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
             // use the existing strategy.
-            new MinificationPackageNamingStrategy(),
+            new MinificationPackageNamingStrategy(appView),
             mappedClasses);
     ClassRenaming classRenaming =
         classNameMinifier.computeRenaming(timing, syntheticCompanionClasses);
@@ -205,16 +204,11 @@
     }
 
     @Override
-    public DexString next(Namespace namespace, DexType type, char[] packagePrefix) {
+    public DexString next(DexType type, char[] packagePrefix, InternalNamingState state) {
       return mappings.getOrDefault(type, type.descriptor);
     }
 
     @Override
-    public boolean bypassDictionary() {
-      return true;
-    }
-
-    @Override
     public boolean noObfuscation(DexType type) {
       // We have an explicit mapping from the proguard map thus everything might have to be renamed.
       return false;
@@ -235,12 +229,12 @@
     }
 
     @Override
-    public DexString next(DexMethod method, MemberNamingInternalState internalState) {
+    public DexString next(DexMethod method, InternalNamingState internalState) {
       return next(method);
     }
 
     @Override
-    public DexString next(DexField field, MemberNamingInternalState internalState) {
+    public DexString next(DexField field, InternalNamingState internalState) {
       return next(field);
     }
 
diff --git a/src/test/java/com/android/tools/r8/naming/ClassObfuscationDictionaryDuplicateTest.java b/src/test/java/com/android/tools/r8/naming/ClassObfuscationDictionaryDuplicateTest.java
new file mode 100644
index 0000000..4f3a868
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/ClassObfuscationDictionaryDuplicateTest.java
@@ -0,0 +1,79 @@
+// 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 com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static junit.framework.TestCase.assertTrue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+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.FileUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.HashSet;
+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 ClassObfuscationDictionaryDuplicateTest extends TestBase {
+
+  public static class A {}
+
+  public static class B {}
+
+  public static class C {
+    public static void main(String[] args) {
+      System.out.print("HELLO WORLD!");
+    }
+  }
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().build();
+  }
+
+  public ClassObfuscationDictionaryDuplicateTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws IOException, CompilationFailedException, ExecutionException {
+    Path dictionary = temp.getRoot().toPath().resolve("dictionary.txt");
+    FileUtils.writeTextFile(dictionary, "a");
+
+    Set<String> finalNames = new HashSet<>();
+    finalNames.add(A.class.getPackage().getName() + ".a");
+    finalNames.add(B.class.getPackage().getName() + ".b");
+
+    testForR8(parameters.getBackend())
+        .addProgramClasses(A.class, B.class, C.class)
+        .noTreeShaking()
+        .addKeepRules("-classobfuscationdictionary " + dictionary.toString())
+        .addKeepMainRule(C.class)
+        .setMinApi(parameters.getRuntime())
+        .compile()
+        .run(parameters.getRuntime(), C.class)
+        .assertSuccessWithOutput("HELLO WORLD!")
+        .inspect(
+            inspector -> {
+              ClassSubject clazzA = inspector.clazz(A.class);
+              assertThat(clazzA, isPresent());
+              assertTrue(finalNames.contains(clazzA.getFinalName()));
+              finalNames.remove(clazzA.getFinalName());
+              ClassSubject clazzB = inspector.clazz(B.class);
+              assertThat(clazzB, isPresent());
+              assertTrue(finalNames.contains(clazzB.getFinalName()));
+            });
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/PackageObfuscationDictionaryDuplicateTest.java b/src/test/java/com/android/tools/r8/naming/PackageObfuscationDictionaryDuplicateTest.java
new file mode 100644
index 0000000..f10947e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/PackageObfuscationDictionaryDuplicateTest.java
@@ -0,0 +1,65 @@
+// 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 junit.framework.TestCase.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.naming.keeppackagenames.Top;
+import com.android.tools.r8.naming.packageobfucationdict.A;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.concurrent.ExecutionException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class PackageObfuscationDictionaryDuplicateTest extends TestBase {
+
+  public static class C {
+    public static void main(String[] args) {
+      System.out.print("HELLO WORLD!");
+    }
+  }
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().build();
+  }
+
+  public PackageObfuscationDictionaryDuplicateTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws IOException, CompilationFailedException, ExecutionException {
+    Path dictionary = temp.getRoot().toPath().resolve("dictionary.txt");
+    FileUtils.writeTextFile(dictionary, "a");
+    testForR8(parameters.getBackend())
+        .addProgramClassesAndInnerClasses(Top.class, A.class, C.class)
+        .noTreeShaking()
+        .addKeepRules("-packageobfuscationdictionary " + dictionary.toString())
+        .addKeepMainRule(C.class)
+        .setMinApi(parameters.getRuntime())
+        .compile()
+        .run(parameters.getRuntime(), C.class)
+        .assertSuccessWithOutput("HELLO WORLD!")
+        .inspect(
+            inspector -> {
+              ClassSubject clazzTop = inspector.clazz(Top.class);
+              assertTrue(clazzTop.getFinalName().endsWith(".a.a"));
+              ClassSubject clazzA = inspector.clazz(A.class);
+              assertTrue(clazzA.getFinalName().endsWith(".b.a"));
+            });
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/packageobfucationdict/A.java b/src/test/java/com/android/tools/r8/naming/packageobfucationdict/A.java
new file mode 100644
index 0000000..012140e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/packageobfucationdict/A.java
@@ -0,0 +1,7 @@
+// 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.packageobfucationdict;
+
+public class A {}