Add support flattening package hierarchy.

Based on http://go/r8-pkg-obfuscation, this CL implements flattening
(as well as repackaging) packages (in a slightly different way).

* Repackaging
In ClassNameMinifier constructor, a top-level state is introduced.
That top-level state's target package prefix is user-given.
Then, when searching for a state for any can-be-renamed classes,
that top-level state is returned, hence all those classes share the same
name space.

* Flattening
The only difference is, for different packages, different states are
created and mapped whose package prefix is suggested by the top-level
state.  That is, classes in each pacakge are still in different name
spaces, while those packages share the same top-level name space.

Bug: 37764746
Change-Id: I37246fecbbbaa38a85cd6701d368072beead478b
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 29b791d..632aa6f 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
@@ -3,10 +3,12 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.naming;
 
+import static com.android.tools.r8.utils.DescriptorUtils.getClassBinaryNameFromDescriptor;
+import static com.android.tools.r8.utils.DescriptorUtils.getDescriptorFromClassBinaryName;
+import static com.android.tools.r8.utils.DescriptorUtils.getPackageBinaryNameFromJavaType;
+
 import com.android.tools.r8.graph.DexAnnotation;
 import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexEncodedField;
-import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
@@ -18,8 +20,9 @@
 import com.android.tools.r8.naming.signature.GenericSignatureParser;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
-import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.InternalOptions.PackageObfuscationMode;
 import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import java.util.Collections;
@@ -34,7 +37,7 @@
 
   private final AppInfoWithLiveness appInfo;
   private final RootSet rootSet;
-  private final String packagePrefix;
+  private final PackageObfuscationMode packageObfuscationMode;
   private final Set<DexString> usedTypeNames = Sets.newIdentityHashSet();
 
   private final Map<DexType, DexString> renaming = Maps.newIdentityHashMap();
@@ -42,39 +45,59 @@
   private final List<String> dictionary;
   private final boolean keepInnerClassStructure;
 
+  private final ClassNamingState topLevelState;
+
   private GenericSignatureRewriter genericSignatureRewriter = new GenericSignatureRewriter();
 
   private GenericSignatureParser<DexType> genericSignatureParser =
       new GenericSignatureParser<>(genericSignatureRewriter);
 
-  ClassNameMinifier(AppInfoWithLiveness appInfo, RootSet rootSet, String packagePrefix,
-      List<String> dictionary, boolean keepInnerClassStructure) {
+  ClassNameMinifier(
+      AppInfoWithLiveness appInfo,
+      RootSet rootSet,
+      PackageObfuscationMode packageObfuscationMode,
+      String packagePrefix,
+      List<String> dictionary,
+      boolean keepInnerClassStructure) {
     this.appInfo = appInfo;
     this.rootSet = rootSet;
-    this.packagePrefix = packagePrefix;
+    this.packageObfuscationMode = packageObfuscationMode;
     this.dictionary = dictionary;
     this.keepInnerClassStructure = keepInnerClassStructure;
+
+    // Initialize top-level naming state.
+    topLevelState = new ClassNamingState(getPackageBinaryNameFromJavaType(packagePrefix));
+    states.computeIfAbsent("", k -> topLevelState);
   }
 
-  Map<DexType, DexString> computeRenaming() {
+  Map<DexType, DexString> computeRenaming(Timing timing) {
     Iterable<DexProgramClass> classes = appInfo.classes();
     // Collect names we have to keep.
+    timing.begin("reserve");
     for (DexClass clazz : classes) {
       if (rootSet.noObfuscation.contains(clazz)) {
         assert !renaming.containsKey(clazz.type);
         registerClassAsUsed(clazz.type);
       }
     }
+    timing.end();
+
+    timing.begin("rename-classes");
     for (DexClass clazz : classes) {
       if (!renaming.containsKey(clazz.type)) {
         DexString renamed = computeName(clazz);
         renaming.put(clazz.type, renamed);
       }
     }
+    timing.end();
 
+    timing.begin("rename-generic");
     renameTypesInGenericSignatures();
+    timing.end();
 
+    timing.begin("rename-arrays");
     appInfo.dexItemFactory.forAllTypes(this::renameArrayTypeIfNeeded);
+    timing.end();
 
     return Collections.unmodifiableMap(renaming);
   }
@@ -162,27 +185,41 @@
       }
     }
     if (state == null) {
-      String packageName = getPackageNameFor(clazz);
-      state = getStateFor(packageName);
+      state = getStateFor(clazz);
     }
     return state.nextTypeName();
   }
 
-  private String getPackageNameFor(DexClass clazz) {
-    if ((packagePrefix == null) || rootSet.keepPackageName.contains(clazz)) {
-      return clazz.type.getPackageDescriptor();
-    } else {
-      return packagePrefix;
+  private ClassNamingState getStateFor(DexClass clazz) {
+    String packageName = getPackageBinaryNameFromJavaType(clazz.type.getPackageDescriptor());
+    // Check whether the given class should be kept.
+    if (rootSet.keepPackageName.contains(clazz)) {
+      return states.computeIfAbsent(packageName, ClassNamingState::new);
     }
-  }
-
-  private ClassNamingState getStateFor(String packageName) {
-    return states.computeIfAbsent(packageName, ClassNamingState::new);
+    ClassNamingState state = topLevelState;
+    switch (packageObfuscationMode) {
+      case NONE:
+        // TODO(b/36799686): general obfuscation.
+        state = states.computeIfAbsent(packageName, ClassNamingState::new);
+        break;
+      case REPACKAGE:
+        // For repackaging, all classes are repackaged to a single package.
+        state = topLevelState;
+        break;
+      case FLATTEN:
+        // For flattening, all packages are repackaged to a single package.
+        state = states.computeIfAbsent(packageName, k -> {
+          String renamedPackageName =
+              getClassBinaryNameFromDescriptor(topLevelState.nextSuggestedName());
+          return new ClassNamingState(renamedPackageName);
+        });
+        break;
+    }
+    return state;
   }
 
   private ClassNamingState getStateForOuterClass(DexType outer) {
-    String prefix = DescriptorUtils
-        .getClassBinaryNameFromDescriptor(outer.toDescriptorString());
+    String prefix = getClassBinaryNameFromDescriptor(outer.toDescriptorString());
     return states.computeIfAbsent(prefix, k -> {
       // Create a naming state with this classes renaming as prefix.
       DexString renamed = renaming.get(outer);
@@ -196,7 +233,7 @@
           renaming.put(outer, renamed);
         }
       }
-      String binaryName = DescriptorUtils.getClassBinaryNameFromDescriptor(renamed.toString());
+      String binaryName = getClassBinaryNameFromDescriptor(renamed.toString());
       return new ClassNamingState(binaryName, "$");
     });
   }
@@ -229,7 +266,8 @@
     }
 
     ClassNamingState(String packageName, String separator) {
-      this.packagePrefix = ("L" + DescriptorUtils.getPackageBinaryNameFromJavaType(packageName)
+      this.packagePrefix = ("L" + packageName
+          // L or La/b/ (or La/b/C$)
           + (packageName.isEmpty() ? "" : separator))
           .toCharArray();
       this.dictionaryIterator = dictionary.iterator();
@@ -239,10 +277,10 @@
       return packagePrefix;
     }
 
-    protected String nextSuggestedName() {
+    String nextSuggestedName() {
       StringBuilder nextName = new StringBuilder();
       if (dictionaryIterator.hasNext()) {
-        nextName.append(getPackagePrefix()).append(dictionaryIterator.next()).append(';');
+        nextName.append(packagePrefix).append(dictionaryIterator.next()).append(';');
         return nextName.toString();
       } else {
         return StringUtils.numberToIdentifier(packagePrefix, typeCounter++, true);
@@ -278,11 +316,9 @@
 
     @Override
     public DexType parsedTypeName(String name) {
-      DexType type = appInfo.dexItemFactory.createType(
-          DescriptorUtils.getDescriptorFromClassBinaryName(name));
+      DexType type = appInfo.dexItemFactory.createType(getDescriptorFromClassBinaryName(name));
       DexString renamedDescriptor = renaming.getOrDefault(type, type.descriptor);
-      renamedSignature.append(DescriptorUtils.getClassBinaryNameFromDescriptor(
-          renamedDescriptor.toString()));
+      renamedSignature.append(getClassBinaryNameFromDescriptor(renamedDescriptor.toString()));
       return type;
     }
 
@@ -292,14 +328,15 @@
       String enclosingDescriptor = enclosingType.toDescriptorString();
       DexType type =
           appInfo.dexItemFactory.createType(
-              DescriptorUtils.getDescriptorFromClassBinaryName(
-                  DescriptorUtils.getClassBinaryNameFromDescriptor(enclosingDescriptor)
+              getDescriptorFromClassBinaryName(
+                  getClassBinaryNameFromDescriptor(enclosingDescriptor)
                   + '$' + name));
       String enclosingRenamedBinaryName =
-          DescriptorUtils.getClassBinaryNameFromDescriptor(renaming.getOrDefault(enclosingType,
-              enclosingType.descriptor).toString());
-      String renamed = DescriptorUtils.getClassBinaryNameFromDescriptor(
-          renaming.getOrDefault(type, type.descriptor).toString());
+          getClassBinaryNameFromDescriptor(
+              renaming.getOrDefault(enclosingType, enclosingType.descriptor).toString());
+      String renamed =
+          getClassBinaryNameFromDescriptor(
+              renaming.getOrDefault(type, type.descriptor).toString());
       assert renamed.startsWith(enclosingRenamedBinaryName + '$');
       String outName = renamed.substring(enclosingRenamedBinaryName.length() + 1);
       renamedSignature.append(outName);
diff --git a/src/main/java/com/android/tools/r8/naming/FieldNameMinifier.java b/src/main/java/com/android/tools/r8/naming/FieldNameMinifier.java
index 8177e89..eec918f 100644
--- a/src/main/java/com/android/tools/r8/naming/FieldNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/FieldNameMinifier.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
+import com.android.tools.r8.utils.Timing;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
@@ -28,19 +29,25 @@
     this.dictionary = dictionary;
   }
 
-  Map<DexField, DexString> computeRenaming() {
+  Map<DexField, DexString> computeRenaming(Timing timing) {
     NamingState<DexType> rootState = NamingState.createRoot(appInfo.dexItemFactory, dictionary);
     // Reserve names in all classes first. We do this in subtyping order so we do not
     // shadow a reserved field in subclasses. While there is no concept of virtual field
     // dispatch in Java, field resolution still traverses the super type chain and external
     // code might use a subtype to reference the field.
+    timing.begin("reserve-classes");
     reserveNamesInSubtypes(appInfo.dexItemFactory.objectType, rootState);
+    timing.end();
     // Next, reserve field names in interfaces. These should only be static.
+    timing.begin("reserve-interfaces");
     DexType.forAllInterfaces(appInfo.dexItemFactory,
         iface -> reserveNamesInSubtypes(iface, rootState));
+    timing.end();
     // Now rename the rest.
+    timing.begin("rename");
     renameFieldsInSubtypes(appInfo.dexItemFactory.objectType);
     DexType.forAllInterfaces(appInfo.dexItemFactory, this::renameFieldsInSubtypes);
+    timing.end();
     return renaming;
   }
 
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 7a4f8f6..b1a15b6 100644
--- a/src/main/java/com/android/tools/r8/naming/Minifier.java
+++ b/src/main/java/com/android/tools/r8/naming/Minifier.java
@@ -42,9 +42,13 @@
     timing.begin("MinifyClasses");
     Map<DexType, DexString> classRenaming =
         new ClassNameMinifier(
-            appInfo, rootSet, options.packagePrefix, options.classObfuscationDictionary,
+            appInfo,
+            rootSet,
+            options.packageObfuscationMode,
+            options.packagePrefix,
+            options.classObfuscationDictionary,
             options.attributeRemoval.signature)
-            .computeRenaming();
+            .computeRenaming(timing);
     timing.end();
     timing.begin("MinifyMethods");
     Map<DexMethod, DexString> methodRenaming =
@@ -53,7 +57,8 @@
     timing.end();
     timing.begin("MinifyFields");
     Map<DexField, DexString> fieldRenaming =
-        new FieldNameMinifier(appInfo, rootSet, options.obfuscationDictionary).computeRenaming();
+        new FieldNameMinifier(appInfo, rootSet, options.obfuscationDictionary)
+            .computeRenaming(timing);
     timing.end();
     return new MinifiedRenaming(classRenaming, methodRenaming, fieldRenaming, appInfo);
   }
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 72ad9f3..642b459 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
@@ -21,7 +21,7 @@
     private final List<Path> injars = new ArrayList<>();
     private final List<Path> libraryjars = new ArrayList<>();
     private PackageObfuscationMode packageObfuscationMode = PackageObfuscationMode.NONE;
-    private String packagePrefix = null;
+    private String packagePrefix = "";
     private boolean allowAccessModification = false;
     private boolean ignoreWarnings = false;
     private boolean optimize = true;
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 1ab82f6..df2125f 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -207,8 +207,6 @@
             configurationBuilder.setFlattenPackagePrefix("");
           }
         }
-        // TODO(b/37764746): warn until package flattening is implemented and in effect.
-        warnIgnoringOptions("flattenpackagehierarchy");
       } else if (acceptString("allowaccessmodification")) {
         configurationBuilder.setAllowAccessModification(true);
       } else if (acceptString("printmapping")) {
diff --git a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
index 3e7c8dd..84beae5 100644
--- a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
@@ -117,7 +117,7 @@
   }
 
   /**
-   * Get class name from its descriptor.
+   * Get simple class name from its descriptor.
    *
    * @param classDescriptor a class descriptor i.e. "Ljava/lang/Object;"
    * @return class name i.e. "Object"
@@ -127,6 +127,17 @@
   }
 
   /**
+   * Get class name from its descriptor.
+   *
+   * @param classDescriptor a class descriptor i.e. "Ljava/lang/Object;"
+   * @return full class name i.e. "java.lang.Object"
+   */
+  public static String getClassNameFromDescriptor(String classDescriptor) {
+    return getClassBinaryNameFromDescriptor(classDescriptor)
+        .replace(DESCRIPTOR_PACKAGE_SEPARATOR, JAVA_PACKAGE_SEPARATOR);
+  }
+
+  /**
    * Get package java name from a class descriptor.
    *
    * @param descriptor a class descriptor i.e. "Ljava/lang/Object;"
@@ -152,7 +163,7 @@
    * Convert package name to a binary name.
    *
    * @param packageName a package name i.e., "java.lang"
-   * @return java pacakge name in a binary name format, i.e., java/lang
+   * @return java package name in a binary name format, i.e., java/lang
    */
   public static String getPackageBinaryNameFromJavaType(String packageName) {
     return packageName.replace(JAVA_PACKAGE_SEPARATOR, DESCRIPTOR_PACKAGE_SEPARATOR);
@@ -168,7 +179,6 @@
     return ('L' + typeBinaryName + ';');
   }
 
-
   /**
    * Get class name from its binary name.
    *
diff --git a/src/test/examples/naming044/keep-rules-003.txt b/src/test/examples/naming044/keep-rules-003.txt
new file mode 100644
index 0000000..10b2856
--- /dev/null
+++ b/src/test/examples/naming044/keep-rules-003.txt
@@ -0,0 +1,11 @@
+# Copyright (c) 2017, 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.
+
+-allowaccessmodification
+
+-flattenpackagehierarchy ''
+
+-keep,allowobfuscation class * {
+  *;
+}
diff --git a/src/test/examples/naming044/keep-rules-004.txt b/src/test/examples/naming044/keep-rules-004.txt
new file mode 100644
index 0000000..3785014
--- /dev/null
+++ b/src/test/examples/naming044/keep-rules-004.txt
@@ -0,0 +1,11 @@
+# Copyright (c) 2017, 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.
+
+-allowaccessmodification
+
+-flattenpackagehierarchy 'p44.y'
+
+-keep,allowobfuscation class * {
+  *;
+}
diff --git a/src/test/examples/naming044/sub/SubA.java b/src/test/examples/naming044/sub/SubA.java
new file mode 100644
index 0000000..c9fac4a
--- /dev/null
+++ b/src/test/examples/naming044/sub/SubA.java
@@ -0,0 +1,8 @@
+// Copyright (c) 2017, 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 naming044.sub;
+
+public class SubA {
+  static int f = 9;
+}
diff --git a/src/test/examples/naming044/sub/SubB.java b/src/test/examples/naming044/sub/SubB.java
new file mode 100644
index 0000000..badc5f2
--- /dev/null
+++ b/src/test/examples/naming044/sub/SubB.java
@@ -0,0 +1,10 @@
+// Copyright (c) 2017, 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 naming044.sub;
+
+public class SubB {
+  public static int n() {
+    return SubA.f;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/PackageNamingTest.java b/src/test/java/com/android/tools/r8/naming/PackageNamingTest.java
index 3e9b269..feaf0d9 100644
--- a/src/test/java/com/android/tools/r8/naming/PackageNamingTest.java
+++ b/src/test/java/com/android/tools/r8/naming/PackageNamingTest.java
@@ -3,8 +3,10 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.naming;
 
-import static org.junit.Assert.assertNotEquals;
+import static com.android.tools.r8.utils.DescriptorUtils.getPackageNameFromDescriptor;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.graph.DexItemFactory;
@@ -12,6 +14,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.Timing;
+import com.google.common.base.CharMatcher;
 import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.Collection;
@@ -47,26 +50,89 @@
     Map<String, BiConsumer<DexItemFactory, NamingLens>> inspections = new HashMap<>();
     inspections.put("naming044:keep-rules-001.txt", PackageNamingTest::test044_rule001);
     inspections.put("naming044:keep-rules-002.txt", PackageNamingTest::test044_rule002);
+    inspections.put("naming044:keep-rules-003.txt", PackageNamingTest::test044_rule003);
+    inspections.put("naming044:keep-rules-004.txt", PackageNamingTest::test044_rule004);
 
     return createTests(tests, inspections);
   }
 
+  private static int countPackageDepth(String descriptor) {
+    return CharMatcher.is('/').countIn(descriptor);
+  }
+
+  // repackageclasses ''
   private static void test044_rule001(DexItemFactory dexItemFactory, NamingLens naming) {
-    DexType b = dexItemFactory.createType("Lnaming044/B;");
     // All classes are moved to the top-level package, hence no package separator.
+    DexType b = dexItemFactory.createType("Lnaming044/B;");
     assertFalse(naming.lookupDescriptor(b).toSourceString().contains("/"));
 
+    // Even classes in a sub-package are moved to the same top-level package.
+    DexType sub = dexItemFactory.createType("Lnaming044/sub/SubB;");
+    assertFalse(naming.lookupDescriptor(sub).toSourceString().contains("/"));
+
+    // method naming044.B.m would be renamed.
     DexMethod m = dexItemFactory.createMethod(
         b, dexItemFactory.createProto(dexItemFactory.intType), "m");
-    // method naming044.B.m would be renamed.
     assertNotEquals("m", naming.lookupName(m).toSourceString());
   }
 
+  // repackageclasses 'p44.x'
   private static void test044_rule002(DexItemFactory dexItemFactory, NamingLens naming) {
+    // All classes are moved to a single package, so they all have the same package prefix.
     DexType a = dexItemFactory.createType("Lnaming044/A;");
     assertTrue(naming.lookupDescriptor(a).toSourceString().startsWith("Lp44/x/"));
 
     DexType b = dexItemFactory.createType("Lnaming044/B;");
     assertTrue(naming.lookupDescriptor(b).toSourceString().startsWith("Lp44/x/"));
+
+    DexType sub = dexItemFactory.createType("Lnaming044/sub/SubB;");
+    assertTrue(naming.lookupDescriptor(sub).toSourceString().startsWith("Lp44/x/"));
+    // Even classes in a sub-package are moved to the same package.
+    assertEquals(
+        getPackageNameFromDescriptor(naming.lookupDescriptor(sub).toSourceString()),
+        getPackageNameFromDescriptor(naming.lookupDescriptor(b).toSourceString()));
+  }
+
+  // flattenpackagehierarchy ''
+  private static void test044_rule003(DexItemFactory dexItemFactory, NamingLens naming) {
+    // All packages are moved to the top-level package, hence only one package separator.
+    DexType b = dexItemFactory.createType("Lnaming044/B;");
+    assertEquals(1, countPackageDepth(naming.lookupDescriptor(b).toSourceString()));
+
+    // Classes in a sub-package are moved to the top-level as well, but in a different one.
+    DexType sub = dexItemFactory.createType("Lnaming044/sub/SubB;");
+    assertEquals(1, countPackageDepth(naming.lookupDescriptor(sub).toSourceString()));
+    assertNotEquals(
+        getPackageNameFromDescriptor(naming.lookupDescriptor(sub).toSourceString()),
+        getPackageNameFromDescriptor(naming.lookupDescriptor(b).toSourceString()));
+
+    // method naming044.B.m would be renamed.
+    DexMethod m = dexItemFactory.createMethod(
+        b, dexItemFactory.createProto(dexItemFactory.intType), "m");
+    assertNotEquals("m", naming.lookupName(m).toSourceString());
+  }
+
+  // flattenpackagehierarchy 'p44.y'
+  private static void test044_rule004(DexItemFactory dexItemFactory, NamingLens naming) {
+    // All packages are moved to a single package.
+    DexType a = dexItemFactory.createType("Lnaming044/A;");
+    assertTrue(naming.lookupDescriptor(a).toSourceString().startsWith("Lp44/y/"));
+    // naming004.A -> Lp44/y/a/a;
+    assertEquals(3, countPackageDepth(naming.lookupDescriptor(a).toSourceString()));
+
+    DexType b = dexItemFactory.createType("Lnaming044/B;");
+    assertTrue(naming.lookupDescriptor(b).toSourceString().startsWith("Lp44/y/"));
+    // naming004.B -> Lp44/y/a/b;
+    assertEquals(3, countPackageDepth(naming.lookupDescriptor(b).toSourceString()));
+
+    DexType sub = dexItemFactory.createType("Lnaming044/sub/SubB;");
+    assertTrue(naming.lookupDescriptor(sub).toSourceString().startsWith("Lp44/y/"));
+    // naming004.sub.SubB -> Lp44/y/b/b;
+    assertEquals(3, countPackageDepth(naming.lookupDescriptor(sub).toSourceString()));
+
+    // Classes in a sub-package should be in a different package.
+    assertNotEquals(
+        getPackageNameFromDescriptor(naming.lookupDescriptor(sub).toSourceString()),
+        getPackageNameFromDescriptor(naming.lookupDescriptor(b).toSourceString()));
   }
 }