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()));
}
}