Class merging across nests
- Disable Vertical class merger when merging
across nests or from nest to non-nest.
- Disable Horizontal class merger when merging
a class with private methods in a nest.
- Add tests for all cases
Bug:132670472
Change-Id: Id73d28c5179d22a0b30148d4d38ca98321b5c856
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index 3f3577a..9a1ea4c 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -855,8 +855,10 @@
if (isNestMember()) {
return nestHost.getNestHost();
}
- assert isNestHost();
- return type;
+ if (isNestHost()) {
+ return type;
+ }
+ return null;
}
public NestHostClassAttribute getNestHostClassAttribute() {
diff --git a/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java b/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
index b526695..3940537 100644
--- a/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
@@ -273,6 +273,10 @@
if (!clazz.virtualMethods().stream().allMatch(DexEncodedMethod::isPrivateMethod)) {
return MergeGroup.DONT_MERGE;
}
+ if (clazz.isInANest()) {
+ // Breaks nest access control, abort merging.
+ return MergeGroup.DONT_MERGE;
+ }
if (Streams.stream(clazz.methods())
.anyMatch(
method ->
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
index 085acee..3e667a2 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -131,6 +131,7 @@
ALWAYS_INLINE,
CONFLICT,
ILLEGAL_ACCESS,
+ MERGE_ACROSS_NESTS,
NATIVE_METHOD,
NO_SIDE_EFFECTS,
PINNED_SOURCE,
@@ -161,6 +162,9 @@
case ILLEGAL_ACCESS:
message = "it could lead to illegal accesses";
break;
+ case MERGE_ACROSS_NESTS:
+ message = "cannot merge across nests, or from nest to non-nest";
+ break;
case NATIVE_METHOD:
message = "it has a native method";
break;
@@ -407,6 +411,15 @@
}
return false;
}
+ DexClass targetClass = appView.definitionFor(singleSubtype);
+ // We abort class merging when merging across nests or from a nest to non-nest.
+ // Without nest this checks null == null.
+ if (targetClass.getNestHost() != clazz.getNestHost()) {
+ if (Log.ENABLED) {
+ AbortReason.MERGE_ACROSS_NESTS.printLogMessageForClass(clazz);
+ }
+ return false;
+ }
return true;
}
diff --git a/src/test/examplesJava11/nestHostExample/BasicNestHostClassMerging.java b/src/test/examplesJava11/nestHostExample/BasicNestHostClassMerging.java
index 0f01e9a..727973b 100644
--- a/src/test/examplesJava11/nestHostExample/BasicNestHostClassMerging.java
+++ b/src/test/examplesJava11/nestHostExample/BasicNestHostClassMerging.java
@@ -19,6 +19,7 @@
public static class InnerMost extends MiddleInner {
+ @NeverInline
public String getFields() {
return ((BasicNestHostClassMerging) this).field
+ ((MiddleOuter) this).field
diff --git a/src/test/examplesJava11/nestHostExample/BasicNestHostTreePruning.java b/src/test/examplesJava11/nestHostExample/BasicNestHostTreePruning.java
index 7c0ca32..286010a 100644
--- a/src/test/examplesJava11/nestHostExample/BasicNestHostTreePruning.java
+++ b/src/test/examplesJava11/nestHostExample/BasicNestHostTreePruning.java
@@ -6,6 +6,7 @@
public static class NotPruned extends BasicNestHostTreePruning {
+ @NeverInline
public String getFields() {
return ((BasicNestHostTreePruning) this).field;
}
diff --git a/src/test/examplesJava11/nestHostExample/NestHostInlining.java b/src/test/examplesJava11/nestHostExample/NestHostInlining.java
new file mode 100644
index 0000000..97a4398
--- /dev/null
+++ b/src/test/examplesJava11/nestHostExample/NestHostInlining.java
@@ -0,0 +1,31 @@
+package nestHostExample;
+
+public class NestHostInlining {
+
+ private String field = "inlining";
+
+ public static class InnerWithPrivAccess {
+ public String access(NestHostInlining host) {
+ return host.field;
+ }
+ }
+
+ public static class InnerNoPrivAccess {
+ public String print() {
+ return "InnerNoPrivAccess";
+ }
+ }
+
+ public abstract static class EmptyNoPrivAccess {}
+
+ public abstract static class EmptyWithPrivAccess {
+ public String access(NestHostInlining host) {
+ return host.field;
+ }
+ }
+
+ public static void main(String[] args) {
+ System.out.println(new InnerWithPrivAccess().access(new NestHostInlining()));
+ System.out.println(new InnerNoPrivAccess().print());
+ }
+}
diff --git a/src/test/examplesJava11/nestHostExample/NestHostInliningSubclasses.java b/src/test/examplesJava11/nestHostExample/NestHostInliningSubclasses.java
new file mode 100644
index 0000000..fd31297
--- /dev/null
+++ b/src/test/examplesJava11/nestHostExample/NestHostInliningSubclasses.java
@@ -0,0 +1,22 @@
+package nestHostExample;
+
+public class NestHostInliningSubclasses {
+
+ public static class InnerWithPrivAccess extends NestHostInlining.InnerWithPrivAccess {
+ public String accessSubclass(NestHostInlining host) {
+ return super.access(host) + "Subclass";
+ }
+ }
+
+ public static class InnerNoPrivAccess extends NestHostInlining.InnerNoPrivAccess {
+ @NeverInline
+ public String printSubclass() {
+ return super.print() + "Subclass";
+ }
+ }
+
+ public static void main(String[] args) {
+ System.out.println(new InnerWithPrivAccess().accessSubclass(new NestHostInlining()));
+ System.out.println(new InnerNoPrivAccess().printSubclass());
+ }
+}
diff --git a/src/test/examplesJava11/nestHostExample/NeverInline.java b/src/test/examplesJava11/nestHostExample/NeverInline.java
new file mode 100644
index 0000000..809b9e7
--- /dev/null
+++ b/src/test/examplesJava11/nestHostExample/NeverInline.java
@@ -0,0 +1,10 @@
+// Copyright (c) 2018, 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 nestHostExample;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+@Target({ElementType.METHOD})
+public @interface NeverInline {}
diff --git a/src/test/examplesJava11/nestHostExample/OutsideInliningNoAccess.java b/src/test/examplesJava11/nestHostExample/OutsideInliningNoAccess.java
new file mode 100644
index 0000000..deb0548
--- /dev/null
+++ b/src/test/examplesJava11/nestHostExample/OutsideInliningNoAccess.java
@@ -0,0 +1,8 @@
+package nestHostExample;
+
+public class OutsideInliningNoAccess extends NestHostInlining.EmptyNoPrivAccess {
+
+ public static void main(String[] args) {
+ System.out.println("OutsideInliningNoAccess");
+ }
+}
diff --git a/src/test/examplesJava11/nestHostExample/OutsideInliningWithAccess.java b/src/test/examplesJava11/nestHostExample/OutsideInliningWithAccess.java
new file mode 100644
index 0000000..92f8ca8
--- /dev/null
+++ b/src/test/examplesJava11/nestHostExample/OutsideInliningWithAccess.java
@@ -0,0 +1,9 @@
+package nestHostExample;
+
+public class OutsideInliningWithAccess extends NestHostInlining.EmptyWithPrivAccess {
+
+ public static void main(String[] args) {
+ System.out.println("OutsideInliningNoAccess");
+ System.out.println(new OutsideInliningWithAccess().access(new NestHostInlining()));
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index 86e3121..a587685 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -174,11 +174,15 @@
}
public T enableInliningAnnotations() {
+ return enableInliningAnnotations(NeverInline.class.getPackage().getName());
+ }
+
+ public T enableInliningAnnotations(String annotationPackageName) {
if (!enableInliningAnnotations) {
enableInliningAnnotations = true;
addInternalKeepRules(
- "-forceinline class * { @com.android.tools.r8.ForceInline *; }",
- "-neverinline class * { @com.android.tools.r8.NeverInline *; }");
+ "-forceinline class * { @" + annotationPackageName + ".ForceInline *; }",
+ "-neverinline class * { @" + annotationPackageName + ".NeverInline *; }");
}
return self();
}
diff --git a/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/FullNestOnProgramPathTest.java b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/FullNestOnProgramPathTest.java
index 21ee28e..b2adfce 100644
--- a/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/FullNestOnProgramPathTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/FullNestOnProgramPathTest.java
@@ -156,18 +156,22 @@
// Interface method desugaring may add extra classes
assertTrue(NUMBER_OF_TEST_CLASSES <= inspector.allClasses().size());
ImmutableList<String> outerClassNames = MAIN_CLASSES.values().asList();
+ ImmutableList<String> nonNestClasses =
+ ImmutableList.of("NeverInline", "OutsideInliningNoAccess", "OutsideInliningWithAccess");
inspector.forAllClasses(
classSubject -> {
DexClass dexClass = classSubject.getDexClass();
- assertTrue(dexClass.isInANest());
- if (outerClassNames.contains(dexClass.type.getName())) {
- assertNull(dexClass.getNestHostClassAttribute());
- assertFalse(dexClass.getNestMembersClassAttributes().isEmpty());
- } else {
- assertTrue(dexClass.getNestMembersClassAttributes().isEmpty());
- assertTrue(
- outerClassNames.contains(
- dexClass.getNestHostClassAttribute().getNestHost().getName()));
+ if (!nonNestClasses.contains(dexClass.type.getName())) {
+ assertTrue(dexClass.isInANest());
+ if (outerClassNames.contains(dexClass.type.getName())) {
+ assertNull(dexClass.getNestHostClassAttribute());
+ assertFalse(dexClass.getNestMembersClassAttributes().isEmpty());
+ } else {
+ assertTrue(dexClass.getNestMembersClassAttributes().isEmpty());
+ assertTrue(
+ outerClassNames.contains(
+ dexClass.getNestHostClassAttribute().getNestHost().getName()));
+ }
}
});
}
diff --git a/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestAccessControlTestUtils.java b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestAccessControlTestUtils.java
index 9d99c5b..45a6195 100644
--- a/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestAccessControlTestUtils.java
+++ b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestAccessControlTestUtils.java
@@ -43,6 +43,16 @@
"BasicNestHostTreePruning",
"BasicNestHostTreePruning$Pruned",
"BasicNestHostTreePruning$NotPruned",
+ "NestHostInlining",
+ "NestHostInlining$InnerWithPrivAccess",
+ "NestHostInlining$InnerNoPrivAccess",
+ "NestHostInlining$EmptyNoPrivAccess",
+ "NestHostInlining$EmptyWithPrivAccess",
+ "NestHostInliningSubclasses",
+ "NestHostInliningSubclasses$InnerWithPrivAccess",
+ "NestHostInliningSubclasses$InnerNoPrivAccess",
+ "OutsideInliningNoAccess",
+ "OutsideInliningWithAccess",
"NestHostExample",
"NestHostExample$NestMemberInner",
"NestHostExample$NestMemberInner$NestMemberInnerInner",
@@ -65,6 +75,8 @@
.put("all", "NestHostExample")
.put("merge", "BasicNestHostClassMerging")
.put("prune", "BasicNestHostTreePruning")
+ .put("inlining", "NestHostInlining")
+ .put("inliningSub", "NestHostInliningSubclasses")
.build();
public static final String ALL_RESULT_LINE =
String.join(
diff --git a/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestAttributesUpdateTest.java b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestAttributesUpdateTest.java
index 06f0a01..3239a4a 100644
--- a/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestAttributesUpdateTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestAttributesUpdateTest.java
@@ -6,6 +6,7 @@
import static com.android.tools.r8.desugar.nestaccesscontrol.NestAccessControlTestUtils.PACKAGE_NAME;
import static com.android.tools.r8.desugar.nestaccesscontrol.NestAccessControlTestUtils.classesMatching;
+import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertNotNull;
import static junit.framework.TestCase.assertSame;
import static junit.framework.TestCase.assertTrue;
@@ -49,49 +50,65 @@
@Test
public void testClassMergingNestMemberRemoval() throws Exception {
- testNestAttributesCorrect(MERGING_OUTER_CLASS, MERGING_OUTER_CLASS, MERGING_EXPECTED_RESULT);
+ testNestAttributesCorrect(MERGING_OUTER_CLASS, MERGING_OUTER_CLASS, MERGING_EXPECTED_RESULT, 3);
}
@Test
public void testClassMergingNestHostRemoval() throws Exception {
testNestAttributesCorrect(
- MERGING_OUTER_CLASS + "$MiddleOuter", MERGING_OUTER_CLASS, MERGING_EXPECTED_RESULT);
+ MERGING_OUTER_CLASS + "$MiddleOuter", MERGING_OUTER_CLASS, MERGING_EXPECTED_RESULT, 2);
}
@Test
public void testTreePruningNestMemberRemoval() throws Exception {
- testNestAttributesCorrect(PRUNING_OUTER_CLASS, PRUNING_OUTER_CLASS, PRUNING_EXPECTED_RESULT);
+ testNestAttributesCorrect(PRUNING_OUTER_CLASS, PRUNING_OUTER_CLASS, PRUNING_EXPECTED_RESULT, 2);
}
@Test
public void testTreePruningNestHostRemoval() throws Exception {
testNestAttributesCorrect(
- PRUNING_OUTER_CLASS + "$Pruned", PRUNING_OUTER_CLASS, PRUNING_EXPECTED_RESULT);
+ PRUNING_OUTER_CLASS + "$Pruned", PRUNING_OUTER_CLASS, PRUNING_EXPECTED_RESULT, 1);
}
public void testNestAttributesCorrect(
- String mainClassName, String outerNestName, String expectedResult) throws Exception {
- testNestAttributesCorrect(mainClassName, outerNestName, expectedResult, true);
- testNestAttributesCorrect(mainClassName, outerNestName, expectedResult, false);
+ String mainClassName, String outerNestName, String expectedResult, int expectedNumClassesLeft)
+ throws Exception {
+ testNestAttributesCorrect(
+ mainClassName, outerNestName, expectedResult, true, expectedNumClassesLeft);
+ testNestAttributesCorrect(
+ mainClassName, outerNestName, expectedResult, false, expectedNumClassesLeft);
}
public void testNestAttributesCorrect(
- String mainClassName, String outerNestName, String expectedResult, boolean minification)
+ String mainClassName,
+ String outerNestName,
+ String expectedResult,
+ boolean minification,
+ int expectedNumClassesLeft)
throws Exception {
String actualMainClassName = PACKAGE_NAME + mainClassName;
testForR8(parameters.getBackend())
.addKeepMainRule(actualMainClassName)
.minification(minification)
.setMinApi(parameters.getApiLevel())
+ .addOptionsModification(
+ options -> {
+ // Disable optimizations else additional classes are removed since they become unused.
+ options.enableValuePropagation = false;
+ options.enableClassInlining = false;
+ })
.addProgramFiles(classesMatching(outerNestName))
- .addOptionsModification(options -> options.enableNestBasedAccessDesugaring = true)
.compile()
- .inspect(this::assertNestAttributesCorrect)
+ .inspect(
+ inspector -> {
+ assertEquals(expectedNumClassesLeft, inspector.allClasses().size());
+ assertNestAttributesCorrect(inspector);
+ })
.run(parameters.getRuntime(), actualMainClassName)
.assertSuccessWithOutput(expectedResult);
}
- private void assertNestAttributesCorrect(CodeInspector inspector) {
+ public static void assertNestAttributesCorrect(CodeInspector inspector) {
assertTrue(inspector.allClasses().size() > 0);
for (FoundClassSubject classSubject : inspector.allClasses()) {
DexClass clazz = classSubject.getDexClass();
diff --git a/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestClassMergingTest.java b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestClassMergingTest.java
new file mode 100644
index 0000000..abc22b7
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/NestClassMergingTest.java
@@ -0,0 +1,109 @@
+package com.android.tools.r8.desugar.nestaccesscontrol;
+
+import static com.android.tools.r8.desugar.nestaccesscontrol.NestAccessControlTestUtils.PACKAGE_NAME;
+import static com.android.tools.r8.desugar.nestaccesscontrol.NestAccessControlTestUtils.classesMatching;
+
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.utils.StringUtils;
+import java.nio.file.Path;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class NestClassMergingTest extends TestBase {
+
+ public NestClassMergingTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ private final TestParameters parameters;
+
+ private final String NEST_MAIN_CLASS = PACKAGE_NAME + "NestHostInlining";
+ private final String NEST_SUBCLASS_MAIN_CLASS = PACKAGE_NAME + "NestHostInliningSubclasses";
+ private final String OUTSIDE_WITH_ACCESS_MAIN_CLASS = PACKAGE_NAME + "OutsideInliningWithAccess";
+ private final String OUTSIDE_NO_ACCESS_MAIN_CLASS = PACKAGE_NAME + "OutsideInliningNoAccess";
+ private final String NEST_MAIN_EXPECTED_RESULT =
+ StringUtils.lines("inlining", "InnerNoPrivAccess");
+ private final String NEST_SUBCLASS_MAIN_EXPECTED_RESULT =
+ StringUtils.lines("inliningSubclass", "InnerNoPrivAccessSubclass");
+ private final String OUTSIDE_WITH_ACCESS_MAIN_EXPECTED_RESULT =
+ StringUtils.lines("OutsideInliningNoAccess", "inlining");
+ private final String OUTSIDE_NO_ACCESS_MAIN_EXPECTED_RESULT =
+ StringUtils.lines("OutsideInliningNoAccess");
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters()
+ .withCfRuntimesStartingFromIncluding(CfVm.JDK11)
+ .withAllApiLevels()
+ .build();
+ }
+
+ @Test
+ public void testClassMergeAcrossTwoNests() throws Exception {
+ // Potentially merge classes from one nest with classes from another nest.
+ testClassMergeAcrossNest(
+ new String[] {NEST_MAIN_CLASS}, new String[] {NEST_MAIN_EXPECTED_RESULT});
+ testClassMergeAcrossNest(
+ new String[] {NEST_SUBCLASS_MAIN_CLASS}, new String[] {NEST_SUBCLASS_MAIN_EXPECTED_RESULT});
+ testClassMergeAcrossNest(
+ new String[] {NEST_MAIN_CLASS, NEST_SUBCLASS_MAIN_CLASS},
+ new String[] {NEST_MAIN_EXPECTED_RESULT, NEST_SUBCLASS_MAIN_EXPECTED_RESULT});
+ }
+
+ @Test
+ public void testClassMergeAcrossNestAndNonNest() throws Exception {
+ // Potentially merge classes from a nest with non nest classes.
+ testClassMergeAcrossNest(
+ new String[] {
+ NEST_MAIN_CLASS, OUTSIDE_NO_ACCESS_MAIN_CLASS, OUTSIDE_WITH_ACCESS_MAIN_CLASS
+ },
+ new String[] {
+ NEST_MAIN_EXPECTED_RESULT,
+ OUTSIDE_NO_ACCESS_MAIN_EXPECTED_RESULT,
+ OUTSIDE_WITH_ACCESS_MAIN_EXPECTED_RESULT
+ });
+ testClassMergeAcrossNest(
+ new String[] {OUTSIDE_NO_ACCESS_MAIN_CLASS},
+ new String[] {OUTSIDE_NO_ACCESS_MAIN_EXPECTED_RESULT});
+ testClassMergeAcrossNest(
+ new String[] {OUTSIDE_WITH_ACCESS_MAIN_CLASS},
+ new String[] {OUTSIDE_WITH_ACCESS_MAIN_EXPECTED_RESULT});
+ }
+
+ public void testClassMergeAcrossNest(String[] mainClasses, String[] expectedResults)
+ throws Exception {
+ List<Path> bothNestsAndOutsideClassToCompile = classesMatching("Inlining");
+ R8FullTestBuilder r8FullTestBuilder = testForR8(parameters.getBackend());
+ for (String clazz : mainClasses) {
+ r8FullTestBuilder.addKeepMainRule(clazz);
+ }
+ R8TestCompileResult compileResult =
+ r8FullTestBuilder
+ .addOptionsModification(
+ options -> {
+ // Disable optimizations else additional classes are removed since they become
+ // unused.
+ options.enableValuePropagation = false;
+ options.enableClassInlining = false;
+ })
+ .enableInliningAnnotations("nestHostExample")
+ .setMinApi(parameters.getApiLevel())
+ .addProgramFiles(bothNestsAndOutsideClassToCompile)
+ .compile()
+ .inspect(NestAttributesUpdateTest::assertNestAttributesCorrect);
+ for (int i = 0; i < mainClasses.length; i++) {
+ compileResult
+ .run(parameters.getRuntime(), mainClasses[i])
+ .assertSuccessWithOutput(expectedResults[i]);
+ }
+ }
+}