Version 1.5.27
Cherry-pick: Keep properties of non-member classes.
CL: https://r8-review.googlesource.com/c/r8/+/38440
Cherry-pick: Add tests for minifying non-member classes.
CL: https://r8-review.googlesource.com/c/r8/+/38200
Bug: 132460884, 132128436
Change-Id: I91ab716fbaf5a2acb9131216a0b66870f866fb7f
diff --git a/src/main/java/com/android/tools/r8/Version.java b/src/main/java/com/android/tools/r8/Version.java
index a4d3221..3ca15f2 100644
--- a/src/main/java/com/android/tools/r8/Version.java
+++ b/src/main/java/com/android/tools/r8/Version.java
@@ -11,7 +11,7 @@
// This field is accessed from release scripts using simple pattern matching.
// Therefore, changing this field could break our release scripts.
- public static final String LABEL = "1.5.26";
+ public static final String LABEL = "1.5.27";
private Version() {
}
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 d707108..1b50601 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
@@ -25,7 +25,6 @@
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.InternalOptions.PackageObfuscationMode;
import com.android.tools.r8.utils.Timing;
-import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
@@ -115,18 +114,9 @@
timing.begin("rename-classes");
for (DexClass clazz : classes) {
if (!renaming.containsKey(clazz.type)) {
- boolean wasAnonymous = clazz.isAnonymousClass();
- // TreePruner already removed inner-class / enclosing-method attributes for local classes.
- assert !clazz.isLocalClass();
clazz.annotations = clazz.annotations.keepIf(this::isNotKotlinMetadata);
DexString renamed = computeName(clazz.type);
renaming.put(clazz.type, renamed);
- // Then-anonymous class is no longer anonymous after minification. Remaining attributes
- // may make the computation of simple name fail on JVM prior to JDK 9.
- if (wasAnonymous) {
- clazz.removeEnclosingMethod(Predicates.alwaysTrue());
- clazz.removeInnerClasses(attr -> attr.getInner() == clazz.type);
- }
// If the class is a member class and it has used $ separator, its renamed name should have
// the same separator (as long as inner-class attribute is honored).
assert !keepInnerClassStructure
@@ -234,13 +224,6 @@
if (clazz == null) {
return null;
}
- // We do not need to preserve the names for local or anonymous classes, as they do not result
- // in a member type declaration and hence cannot be referenced as nested classes in
- // method signatures.
- // See https://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.5.
- if (clazz.getEnclosingMethod() != null) {
- return null;
- }
// For DEX inputs this could result in returning the outer class of a local class since we
// can't distinguish it from a member class based on just the enclosing-class annotation.
// We could filter out the local classes by looking for a corresponding entry in the
@@ -250,27 +233,25 @@
if (attribute == null) {
return null;
}
- return attribute.getOuter();
- }
-
- private DexString getInnerNameForType(DexType type) {
- // This util is used only after the corresponding outer-class is recognized.
- // Therefore, the definition for the type and its inner-class attribute should be found.
- DexClass clazz = appView.definitionFor(type);
- assert clazz != null;
- InnerClassAttribute attribute = clazz.getInnerClassAttributeForThisClass();
- assert attribute != null;
- return attribute.getInnerName();
+ return attribute.getLiveContext(appView.appInfo());
}
private DexString computeName(DexType type) {
Namespace state = null;
if (keepInnerClassStructure) {
- // When keeping the nesting structure of inner classes, we have to insert the name
- // of the outer class for the $ prefix.
+ // When keeping the nesting structure of inner classes, bind this type to the live context.
+ // Note that such live context might not be always the enclosing class. E.g., a local class
+ // does not have a direct enclosing class, but we use the holder of the enclosing method here.
DexType outerClass = getOutClassForType(type);
if (outerClass != null) {
- String separator = computeInnerClassSeparator(outerClass, type, getInnerNameForType(type));
+ DexClass clazz = appView.definitionFor(type);
+ assert clazz != null;
+ InnerClassAttribute attribute = clazz.getInnerClassAttributeForThisClass();
+ assert attribute != null;
+ // Note that, to be consistent with the way inner-class attribute is written via minifier
+ // lense, we are using attribute's outer-class, not the live context.
+ String separator =
+ computeInnerClassSeparator(attribute.getOuter(), type, attribute.getInnerName());
if (separator == null) {
separator = String.valueOf(INNER_CLASS_SEPARATOR);
}
diff --git a/src/main/java/com/android/tools/r8/naming/MinifiedRenaming.java b/src/main/java/com/android/tools/r8/naming/MinifiedRenaming.java
index f14e456..f1dd450 100644
--- a/src/main/java/com/android/tools/r8/naming/MinifiedRenaming.java
+++ b/src/main/java/com/android/tools/r8/naming/MinifiedRenaming.java
@@ -77,7 +77,6 @@
attribute.getOuter(), innerType, attribute.getInnerName());
if (separator == null) {
separator = String.valueOf(DescriptorUtils.INNER_CLASS_SEPARATOR);
-
}
int index = innerTypeMapped.lastIndexOf(separator);
if (index < 0) {
diff --git a/src/main/java/com/android/tools/r8/shaking/TreePruner.java b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
index 1671197..fa2212d 100644
--- a/src/main/java/com/android/tools/r8/shaking/TreePruner.java
+++ b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
@@ -17,7 +17,6 @@
import com.android.tools.r8.graph.PresortedComparable;
import com.android.tools.r8.logging.Log;
import com.android.tools.r8.utils.InternalOptions;
-import com.google.common.base.Predicates;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.Collection;
@@ -119,17 +118,6 @@
if (reachableStaticFields != null) {
clazz.setStaticFields(reachableStaticFields);
}
- // If the class is local, it'll become an ordinary class by renaming.
- // Invalidate its inner-class / enclosing-method attributes early.
- if (appView.options().isMinifying()
- && appView.rootSet().mayBeMinified(clazz.type, appView)
- && clazz.isLocalClass()) {
- assert clazz.getInnerClassAttributeForThisClass() != null;
- clazz.removeEnclosingMethod(Predicates.alwaysTrue());
- InnerClassAttribute innerClassAttribute =
- clazz.getInnerClassAttributeForThisClass();
- clazz.removeInnerClasses(attr -> attr == innerClassAttribute);
- }
clazz.removeInnerClasses(this::isAttributeReferencingPrunedType);
clazz.removeEnclosingMethod(this::isAttributeReferencingPrunedItem);
usagePrinter.visited();
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 725b435..a91cc8b 100644
--- a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
@@ -311,7 +311,9 @@
public static String computeInnerClassSeparator(
DexType outerClass, DexType innerClass, DexString innerName) {
- if (innerName == null) {
+ assert innerClass != null;
+ // Filter out non-member classes ahead.
+ if (outerClass == null || innerName == null) {
return String.valueOf(INNER_CLASS_SEPARATOR);
}
return computeInnerClassSeparator(
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetSimpleNameTest.java b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetSimpleNameTest.java
index de5dd77..ee1edd0 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetSimpleNameTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetSimpleNameTest.java
@@ -14,6 +14,7 @@
import com.android.tools.r8.R8TestRunResult;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.TestRuntime.CfVm;
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.utils.StringUtils;
import com.android.tools.r8.utils.codeinspector.ClassSubject;
@@ -134,7 +135,11 @@
"Outer$Inner",
"Outer$Inner"
);
- private static final String RENAMED_OUTPUT = StringUtils.lines(
+ // JDK8 computes the simple name differently: some assumptions about non-member classes,
+ // e.g., 1 or more digits (followed by the simple name if it's local).
+ // Since JDK9, the simple name is computed by stripping off the package name.
+ // See b/132808897 for more details.
+ private static final String RENAMED_OUTPUT_JDK8 = StringUtils.lines(
"d",
"a",
"a",
@@ -144,6 +149,16 @@
"a",
"a"
);
+ private static final String RENAMED_OUTPUT = StringUtils.lines(
+ "d",
+ "a",
+ "a",
+ "a",
+ "c[][][]",
+ "[][][]",
+ "a",
+ "a"
+ );
private static final Class<?> MAIN = ClassGetSimpleName.class;
public GetSimpleNameTest(TestParameters parameters, boolean enableMinification) throws Exception {
@@ -245,7 +260,12 @@
.addOptionsModification(this::configure)
.run(parameters.getRuntime(), MAIN);
if (enableMinification) {
- result.assertSuccessWithOutput(RENAMED_OUTPUT);
+ if (parameters.isCfRuntime()
+ && parameters.getRuntime().asCf().getVm().lessThanOrEqual(CfVm.JDK8)) {
+ result.assertSuccessWithOutput(RENAMED_OUTPUT_JDK8);
+ } else {
+ result.assertSuccessWithOutput(RENAMED_OUTPUT);
+ }
} else {
result.assertSuccessWithOutput(JAVA_OUTPUT);
}
diff --git a/src/test/java/com/android/tools/r8/naming/NonMemberClassTest.java b/src/test/java/com/android/tools/r8/naming/NonMemberClassTest.java
new file mode 100644
index 0000000..04c39b4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/NonMemberClassTest.java
@@ -0,0 +1,199 @@
+// 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.Assume.assumeTrue;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class NonMemberClassTest extends TestBase {
+
+ static class Enclosing {
+ @NeverInline
+ void foo() {
+ class Local {
+ Local() {
+ if (this.getClass().getEnclosingClass() != null) {
+ System.out.println("I'm local.");
+ }
+ }
+ }
+ new Local();
+
+ Runnable r = new Runnable() {
+ @NeverInline
+ @Override
+ public void run() {
+ if (this.getClass().isAnonymousClass()) {
+ System.out.println("I'm anonymous.");
+ }
+ }
+ };
+ r.run();
+ }
+ }
+
+ static class TestMain {
+ public static void main(String... args) {
+ new Enclosing().foo();
+ }
+ }
+
+ enum TestConfig {
+ KEEP_INNER_CLASSES,
+ KEEP_ALLOW_MINIFICATION,
+ NO_KEEP_NO_MINIFICATION,
+ NO_KEEP_MINIFICATION;
+
+ public String getKeepRules() {
+ switch (this) {
+ case KEEP_INNER_CLASSES:
+ return "-keep class " + Enclosing.class.getName() + "$*";
+ case KEEP_ALLOW_MINIFICATION:
+ return "-keep,allowobfuscation class " + Enclosing.class.getName() + "$*";
+ case NO_KEEP_NO_MINIFICATION:
+ return "-dontobfuscate";
+ case NO_KEEP_MINIFICATION:
+ return "";
+ default:
+ throw new Unreachable();
+ }
+ }
+
+ public String getExpectedOutput(TestParameters parameters, boolean isFullMode) {
+ switch (this) {
+ case KEEP_INNER_CLASSES:
+ return JVM_OUTPUT;
+ case KEEP_ALLOW_MINIFICATION:
+ if (parameters.isCfRuntime()
+ && parameters.getRuntime().asCf().getVm().lessThanOrEqual(CfVm.JDK8)) {
+ return MINIFIED_OUTPUT_JDK8;
+ }
+ return JVM_OUTPUT;
+ case NO_KEEP_NO_MINIFICATION:
+ // In full mode, we remove all attributes since nothing pinned, i.e., no reflection uses.
+ return isFullMode ? "" : JVM_OUTPUT;
+ case NO_KEEP_MINIFICATION:
+ // In full mode, we remove all attributes since nothing pinned, i.e., no reflection uses.
+ if (isFullMode) {
+ return "";
+ }
+ if (parameters.isCfRuntime()
+ && parameters.getRuntime().asCf().getVm().lessThanOrEqual(CfVm.JDK8)) {
+ return MINIFIED_OUTPUT_JDK8;
+ }
+ return JVM_OUTPUT;
+ default:
+ throw new Unreachable();
+ }
+ }
+
+ public void inspect(boolean isFullMode, CodeInspector inspector) {
+ int expectedNumberOfNonMemberInnerClasses;
+ switch (this) {
+ case KEEP_INNER_CLASSES:
+ case KEEP_ALLOW_MINIFICATION:
+ expectedNumberOfNonMemberInnerClasses = 2;
+ break;
+ case NO_KEEP_NO_MINIFICATION:
+ case NO_KEEP_MINIFICATION:
+ expectedNumberOfNonMemberInnerClasses = isFullMode ? 0 : 2;
+ break;
+ default:
+ throw new Unreachable();
+ }
+ assertEquals(
+ expectedNumberOfNonMemberInnerClasses,
+ inspector.allClasses().stream().filter(classSubject ->
+ classSubject.getDexClass().isLocalClass()
+ || classSubject.getDexClass().isAnonymousClass()).count());
+ }
+ }
+
+ private static final Class<?> MAIN = TestMain.class;
+
+ // Since JDK9, a class is determined as anonymous if the inner-name in the associated inner-class
+ // attribute is empty.
+ // JDK8 determines an anonymous class differently: checking if a simple name is empty.
+ // Moreover, it computes the simple name differently: some assumptions about non-member classes,
+ // e.g., 1 or more digits (followed by the simple name if it's local).
+ // Since JDK9, the simple name is computed by stripping off the package name.
+ // See b/132808897 for more details.
+ private static final String MINIFIED_OUTPUT_JDK8 = StringUtils.lines(
+ "I'm local."
+ );
+ private static final String JVM_OUTPUT = StringUtils.lines(
+ "I'm local.",
+ "I'm anonymous."
+ );
+
+ private final TestParameters parameters;
+ private final TestConfig config;
+
+ @Parameterized.Parameters(name = "{0} {1}")
+ public static Collection<Object[]> data() {
+ return buildParameters(
+ getTestParameters().withAllRuntimes().withAllApiLevels().build(), TestConfig.values());
+ }
+
+ public NonMemberClassTest(TestParameters parameters, TestConfig config) {
+ this.parameters = parameters;
+ this.config = config;
+ }
+
+ @Test
+ public void testJVMOutput() throws Exception {
+ assumeTrue(
+ "Only run JVM reference on CF runtimes",
+ parameters.isCfRuntime() && config == TestConfig.NO_KEEP_NO_MINIFICATION);
+ testForJvm()
+ .addTestClasspath()
+ .run(parameters.getRuntime(), MAIN)
+ .assertSuccessWithOutput(JVM_OUTPUT);
+ }
+
+ @Test
+ public void testR8Compat() throws Exception {
+ testForR8Compat(parameters.getBackend())
+ .addInnerClasses(NonMemberClassTest.class)
+ .addKeepMainRule(MAIN)
+ .addKeepRules(config.getKeepRules())
+ .addKeepAttributes("Signature", "InnerClasses", "EnclosingMethod")
+ .enableInliningAnnotations()
+ .setMinApi(parameters.getApiLevel())
+ .addOptionsModification(options -> options.enableClassInlining = false)
+ .compile()
+ .inspect(inspector -> config.inspect(false, inspector))
+ .run(parameters.getRuntime(), MAIN)
+ .assertSuccessWithOutput(config.getExpectedOutput(parameters, false));
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ testForR8(parameters.getBackend())
+ .addInnerClasses(NonMemberClassTest.class)
+ .addKeepMainRule(MAIN)
+ .addKeepRules(config.getKeepRules())
+ .addKeepAttributes("Signature", "InnerClasses", "EnclosingMethod")
+ .enableInliningAnnotations()
+ .setMinApi(parameters.getApiLevel())
+ .addOptionsModification(options -> options.enableClassInlining = false)
+ .compile()
+ .inspect(inspector -> config.inspect(true, inspector))
+ .run(parameters.getRuntime(), MAIN)
+ .assertSuccessWithOutput(config.getExpectedOutput(parameters, true));
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/b132460884/LocalClassRenamingTest.java b/src/test/java/com/android/tools/r8/naming/b132460884/LocalClassRenamingTest.java
new file mode 100644
index 0000000..4d82789
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/b132460884/LocalClassRenamingTest.java
@@ -0,0 +1,98 @@
+// 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.b132460884;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+class TestMap<K, V> {
+ private Map<K, V> map;
+
+ private TestMap() {
+ this.map = new HashMap<>();
+ }
+
+ public static <K, V> TestMap of(K k, V v) {
+ TestMap<K, V> m = new TestMap<>();
+ m.map.put(k, v);
+ return m;
+ }
+
+ public Set<Map.Entry<K, V>> entrySet() {
+ class LocalEntrySet extends HashSet<Map.Entry<K, V>> {
+ @Override
+ public Iterator<Map.Entry<K, V>> iterator() {
+ return map.entrySet().iterator();
+ }
+ }
+ return map.isEmpty() ? new HashSet<>() : new LocalEntrySet();
+ }
+}
+
+class TestMain {
+ public static void main(String... args) {
+ TestMap<String, Integer> map = TestMap.of("R", 8);
+ Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry<String, Integer> entry = it.next();
+ System.out.println(entry.getKey() + " -> " + entry.getValue());
+ }
+ }
+}
+
+@RunWith(Parameterized.class)
+public class LocalClassRenamingTest extends TestBase {
+ private static final String EXPECTED_OUTPUT = StringUtils.lines("R -> 8");
+
+ private final TestParameters parameters;
+ private final boolean enableMinification;
+
+ @Parameterized.Parameters(name = "{0} minification: {1}")
+ public static Collection<Object[]> data() {
+ return buildParameters(
+ getTestParameters().withAllRuntimes().withAllApiLevels().build(),
+ BooleanUtils.values());
+ }
+
+ public LocalClassRenamingTest(TestParameters parameters, boolean enableMinification) {
+ this.parameters = parameters;
+ this.enableMinification = enableMinification;
+ }
+
+ @Test
+ public void b132460884() throws Exception {
+ testForR8Compat(parameters.getBackend())
+ .addProgramClassesAndInnerClasses(TestMap.class)
+ .addProgramClasses(TestMain.class)
+ .addKeepMainRule(TestMain.class)
+ .addKeepAttributes("Signature", "InnerClasses")
+ .noTreeShaking()
+ .minification(enableMinification)
+ .setMinApi(parameters.getApiLevel())
+ .compile()
+ .inspect(inspector -> {
+ ClassSubject local = inspector.clazz(TestMap.class.getName() + "$1LocalEntrySet");
+ assertThat(local, isPresent());
+ assertEquals(enableMinification, local.isRenamed());
+ })
+ .run(parameters.getRuntime(), TestMain.class)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/android/tools/r8/shaking/examples/TreeShakingAnnotationremovalTest.java b/src/test/java/com/android/tools/r8/shaking/examples/TreeShakingAnnotationremovalTest.java
index 0a21b8c..52a756b 100644
--- a/src/test/java/com/android/tools/r8/shaking/examples/TreeShakingAnnotationremovalTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/examples/TreeShakingAnnotationremovalTest.java
@@ -96,12 +96,12 @@
Assert.assertFalse(inner.isLocalClass());
ClassSubject anonymous = inspector.clazz("annotationremoval.OuterClass$1");
Assert.assertTrue(anonymous.isPresent());
- Assert.assertEquals(!getMinify().isMinify(), anonymous.isAnonymousClass());
+ Assert.assertTrue(anonymous.isAnonymousClass());
Assert.assertFalse(anonymous.isMemberClass());
Assert.assertFalse(anonymous.isLocalClass());
ClassSubject local = inspector.clazz("annotationremoval.OuterClass$1LocalMagic");
Assert.assertTrue(local.isPresent());
- Assert.assertEquals(!getMinify().isMinify(), local.isLocalClass());
+ Assert.assertTrue(local.isLocalClass());
Assert.assertFalse(local.isMemberClass());
Assert.assertFalse(local.isAnonymousClass());
}