Reland "Only retain class initializers that may be live"
This reverts commit 0c594c3b118317dc4bedb47d0314528e0f0c44b6.
Bug: 144003629
Change-Id: I66e9420fb6e659db77aab36fb747a78347719cbe
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
index ccf312a..be007bb 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -99,7 +99,6 @@
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap;
-import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
import it.unimi.dsi.fastutil.ints.Int2ReferenceMap.Entry;
import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
@@ -1370,6 +1369,10 @@
return;
}
+ if (!appView.options().testing.enableCheckCastAndInstanceOfRemoval) {
+ return;
+ }
+
IRMetadata metadata = code.metadata();
if (!metadata.mayHaveCheckCast() && !metadata.mayHaveInstanceOf()) {
return;
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index 400ce93..76be64f 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -60,6 +60,7 @@
import com.android.tools.r8.shaking.RootSetBuilder.ConsequentRootSet;
import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
import com.android.tools.r8.shaking.ScopedDexMethodSet.AddMethodIfMoreVisibleResult;
+import com.android.tools.r8.utils.DequeUtils;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.SetUtils;
import com.android.tools.r8.utils.StringDiagnostic;
@@ -178,6 +179,9 @@
*/
private final SetWithReportedReason<DexProgramClass> liveTypes;
+ /** Set of types whose class initializer may execute. */
+ private final SetWithReportedReason<DexProgramClass> initializedTypes;
+
/** Set of live types defined in the library and classpath. Used to avoid duplicate tracing. */
private final Set<DexClass> liveNonProgramTypes = Sets.newIdentityHashSet();
@@ -314,6 +318,7 @@
liveTypes = new SetWithReportedReason<>();
liveAnnotations = new SetWithReason<>(graphReporter::registerAnnotation);
+ initializedTypes = new SetWithReportedReason<>();
instantiatedTypes = new SetWithReason<>(graphReporter::registerClass);
targetedMethods = new SetWithReason<>(graphReporter::registerMethod);
// This set is only populated in edge cases due to multiple default interface methods.
@@ -1186,16 +1191,6 @@
// CF libraries can be used by Android apps. See b/136698023 for more information.
ensureMethodsContinueToWidenAccess(holder, seen, reason);
- // We also need to add the corresponding <clinit> to the set of live methods, as otherwise
- // static field initialization (and other class-load-time sideeffects) will not happen.
- if (holder.hasClassInitializer()) {
- DexEncodedMethod clinit = holder.getClassInitializer();
- if (clinit != null && clinit.getOptimizationInfo().mayHaveSideEffects()) {
- assert clinit.method.holder == holder.type;
- markDirectStaticOrConstructorMethodAsLive(holder, clinit, reason);
- }
- }
-
if (holder.isSerializable(appView)) {
enqueueFirstNonSerializableClassInitializer(holder, reason);
}
@@ -1317,17 +1312,45 @@
// Only mark methods for which invocation will succeed at runtime live.
if (encodedMethod.isStatic()) {
- registerClassInitializer(clazz, reason);
+ markDirectAndIndirectClassInitializersAsLive(clazz);
markDirectStaticOrConstructorMethodAsLive(clazz, encodedMethod, reason);
}
}
- private void registerClassInitializer(DexProgramClass definition, KeepReason reason) {
- if (definition.hasClassInitializer()) {
- graphReporter.registerMethod(definition.getClassInitializer(), reason);
+ private void markDirectAndIndirectClassInitializersAsLive(DexProgramClass clazz) {
+ Deque<DexProgramClass> worklist = DequeUtils.newArrayDeque(clazz);
+ Set<DexProgramClass> visited = SetUtils.newIdentityHashSet(clazz);
+ while (!worklist.isEmpty()) {
+ DexProgramClass current = worklist.removeFirst();
+ assert visited.contains(current);
+
+ if (!markDirectClassInitializerAsLive(current)) {
+ continue;
+ }
+
+ // Mark all class initializers in all super types as live.
+ for (DexType superType : clazz.allImmediateSupertypes()) {
+ DexProgramClass superClass = getProgramClassOrNull(superType);
+ if (superClass != null && visited.add(superClass)) {
+ worklist.add(superClass);
+ }
+ }
}
}
+ /** Returns true if the class initializer became live for the first time. */
+ private boolean markDirectClassInitializerAsLive(DexProgramClass clazz) {
+ DexEncodedMethod clinit = clazz.getClassInitializer();
+ KeepReasonWitness witness = graphReporter.reportReachableClassInitializer(clazz, clinit);
+ if (!initializedTypes.add(clazz, witness)) {
+ return false;
+ }
+ if (clinit != null && clinit.getOptimizationInfo().mayHaveSideEffects()) {
+ markDirectStaticOrConstructorMethodAsLive(clazz, clinit, witness);
+ }
+ return true;
+ }
+
// Package protected due to entry point from worklist.
void markNonStaticDirectMethodAsReachable(DexMethod method, KeepReason reason) {
handleInvokeOfDirectTarget(method, reason);
@@ -1463,6 +1486,8 @@
}
// This class becomes live, so it and all its supertypes become live types.
markTypeAsLive(clazz, graphReporter.registerClass(clazz, reason));
+ // Instantiation triggers class initialization.
+ markDirectAndIndirectClassInitializersAsLive(clazz);
// For all methods of the class, if we have seen a call, mark the method live.
// We only do this for virtual calls, as the other ones will be done directly.
transitionMethodsForInstantiatedClass(clazz);
@@ -1708,7 +1733,7 @@
return;
}
- registerClassInitializer(clazz, reason);
+ markDirectAndIndirectClassInitializersAsLive(clazz);
// This field might be an instance field reachable from a static context, e.g. a getStatic that
// resolves to an instance field. We have to keep the instance field nonetheless, as otherwise
@@ -2460,6 +2485,10 @@
return;
}
+ if (method.isStatic()) {
+ markDirectAndIndirectClassInitializersAsLive(clazz);
+ }
+
Set<DexEncodedMethod> superCallTargets = superInvokeDependencies.get(method);
if (superCallTargets != null) {
for (DexEncodedMethod superCallTarget : superCallTargets) {
diff --git a/src/main/java/com/android/tools/r8/shaking/GraphReporter.java b/src/main/java/com/android/tools/r8/shaking/GraphReporter.java
index 521b90c..cf820bc 100644
--- a/src/main/java/com/android/tools/r8/shaking/GraphReporter.java
+++ b/src/main/java/com/android/tools/r8/shaking/GraphReporter.java
@@ -227,6 +227,22 @@
return KeepReasonWitness.INSTANCE;
}
+ public KeepReasonWitness reportReachableClassInitializer(
+ DexProgramClass clazz, DexEncodedMethod initializer) {
+ if (initializer != null) {
+ assert clazz.type == initializer.method.holder;
+ assert initializer.isClassInitializer();
+ if (keptGraphConsumer != null) {
+ ClassGraphNode source = getClassGraphNode(clazz.type);
+ MethodGraphNode target = getMethodGraphNode(initializer.method);
+ return reportEdge(source, target, EdgeKind.ReachableFromLiveType);
+ }
+ } else {
+ assert !clazz.hasClassInitializer();
+ }
+ return KeepReasonWitness.INSTANCE;
+ }
+
public KeepReasonWitness reportReachableMethodAsLive(
DexEncodedMethod encodedMethod, MarkedResolutionTarget reason) {
if (keptGraphConsumer != null) {
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 2815cb8..d8ef9c4 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -971,6 +971,7 @@
public boolean allowUnusedProguardConfigurationRules = true;
public boolean reportUnusedProguardConfigurationRules = false;
public boolean alwaysUsePessimisticRegisterAllocation = false;
+ public boolean enableCheckCastAndInstanceOfRemoval = true;
public boolean enableDeadSwitchCaseElimination = true;
public boolean enableSwitchToIfRewriting = true;
public boolean forceRedundantConstNumberRemoval = false;
diff --git a/src/test/java/com/android/tools/r8/internal/proto/Proto2ShrinkingTest.java b/src/test/java/com/android/tools/r8/internal/proto/Proto2ShrinkingTest.java
index 506e33a..2071b82 100644
--- a/src/test/java/com/android/tools/r8/internal/proto/Proto2ShrinkingTest.java
+++ b/src/test/java/com/android/tools/r8/internal/proto/Proto2ShrinkingTest.java
@@ -78,6 +78,7 @@
.addProgramFiles(PROGRAM_FILES)
.addKeepMainRule("proto2.TestClass")
.addKeepRuleFiles(PROTOBUF_LITE_PROGUARD_RULES)
+ .addKeepRules(allGeneratedMessageLiteSubtypesAreInstantiatedRule())
.addKeepRules(alwaysInlineNewSingularGeneratedExtensionRule())
.addOptionsModification(
options -> {
@@ -85,18 +86,6 @@
options.protoShrinking().enableGeneratedMessageLiteShrinking = true;
options.protoShrinking().enableGeneratedExtensionRegistryShrinking = true;
options.enableStringSwitchConversion = true;
-
- // TODO(b/144003629): If devirtualization is enabled, then we insert a cast to
- // PartiallyUsed$Enum$EnumVerifier in MessageSchema.parseOneofField. This causes
- // us to retain PartiallyUsed$Enum$EnumVerifier.<clinit>(), which creates an
- // instance of PartiallyUsed$Enum$EnumVerifier, which causes the virtual
- // method PartiallyUsed$Enum$EnumVerifier.isInRange() to become live, which in
- // turn causes the type PartiallyUsed$Enum to become live.
- //
- // Note: This is *not* a general problem, since it only manifests if there is only
- // a single proto enum in the entire program. In other tests, this issue does not
- // appear.
- options.enableDevirtualization = false;
})
.allowAccessModification(allowAccessModification)
.allowUnusedProguardConfigurationRules()
diff --git a/src/test/java/com/android/tools/r8/internal/proto/Proto3ShrinkingTest.java b/src/test/java/com/android/tools/r8/internal/proto/Proto3ShrinkingTest.java
index 2a4b79e..7a67427 100644
--- a/src/test/java/com/android/tools/r8/internal/proto/Proto3ShrinkingTest.java
+++ b/src/test/java/com/android/tools/r8/internal/proto/Proto3ShrinkingTest.java
@@ -38,7 +38,7 @@
return buildParameters(
BooleanUtils.values(),
BooleanUtils.values(),
- getTestParameters().withAllRuntimes().build());
+ getTestParameters().withAllRuntimesAndApiLevels().build());
}
public Proto3ShrinkingTest(
@@ -64,7 +64,7 @@
.allowAccessModification(allowAccessModification)
.allowUnusedProguardConfigurationRules()
.minification(enableMinification)
- .setMinApi(parameters.getRuntime())
+ .setMinApi(parameters.getApiLevel())
.compile()
.inspect(
outputInspector -> {
diff --git a/src/test/java/com/android/tools/r8/internal/proto/ProtoShrinkingTestBase.java b/src/test/java/com/android/tools/r8/internal/proto/ProtoShrinkingTestBase.java
index 9a88fe1..19b3857 100644
--- a/src/test/java/com/android/tools/r8/internal/proto/ProtoShrinkingTestBase.java
+++ b/src/test/java/com/android/tools/r8/internal/proto/ProtoShrinkingTestBase.java
@@ -69,6 +69,14 @@
}
}
+ static String allGeneratedMessageLiteSubtypesAreInstantiatedRule() {
+ return StringUtils.lines(
+ "-if class * extends com.google.protobuf.GeneratedMessageLite",
+ "-keep,allowobfuscation class <1> {",
+ " <init>(...);",
+ "}");
+ }
+
static String alwaysInlineNewSingularGeneratedExtensionRule() {
return StringUtils.lines(
"-alwaysinline class com.google.protobuf.GeneratedMessageLite {",
diff --git a/src/test/java/com/android/tools/r8/shaking/clinit/ClassInitializedByClassForNameTest.java b/src/test/java/com/android/tools/r8/shaking/clinit/ClassInitializedByClassForNameTest.java
new file mode 100644
index 0000000..169bfa5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/clinit/ClassInitializedByClassForNameTest.java
@@ -0,0 +1,66 @@
+// 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.shaking.clinit;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+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 ClassInitializedByClassForNameTest extends TestBase {
+
+ private final TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ public ClassInitializedByClassForNameTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void test() throws Exception {
+ testForR8(parameters.getBackend())
+ .addInnerClasses(ClassInitializedByClassForNameTest.class)
+ .addKeepMainRule(TestClass.class)
+ .setMinApi(parameters.getApiLevel())
+ .compile()
+ .inspect(this::inspect)
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutputLines("Hello world!");
+ }
+
+ private void inspect(CodeInspector inspector) {
+ // Check that A.<clinit>() is not removed.
+ ClassSubject aClassSubject = inspector.clazz(A.class);
+ assertThat(aClassSubject, isPresent());
+ assertThat(aClassSubject.clinit(), isPresent());
+ }
+
+ static class TestClass {
+
+ public static void main(String[] args) throws ClassNotFoundException {
+ Class.forName("com.android.tools.r8.shaking.clinit.ClassInitializedByClassForNameTest$A");
+ }
+ }
+
+ static class A {
+
+ static {
+ System.out.println("Hello world!");
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/clinit/ClassInitializedByInvokeStaticTest.java b/src/test/java/com/android/tools/r8/shaking/clinit/ClassInitializedByInvokeStaticTest.java
new file mode 100644
index 0000000..8036f2f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/clinit/ClassInitializedByInvokeStaticTest.java
@@ -0,0 +1,70 @@
+// 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.shaking.clinit;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+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 ClassInitializedByInvokeStaticTest extends TestBase {
+
+ private final TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ public ClassInitializedByInvokeStaticTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void test() throws Exception {
+ testForR8(parameters.getBackend())
+ .addInnerClasses(ClassInitializedByInvokeStaticTest.class)
+ .addKeepMainRule(TestClass.class)
+ .setMinApi(parameters.getApiLevel())
+ .compile()
+ .inspect(this::inspect)
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutputLines("Hello world!");
+ }
+
+ private void inspect(CodeInspector inspector) {
+ // Check that A.<clinit>() is not removed.
+ ClassSubject aClassSubject = inspector.clazz(A.class);
+ assertThat(aClassSubject, isPresent());
+ assertThat(aClassSubject.clinit(), isPresent());
+ }
+
+ static class TestClass {
+
+ public static void main(String[] args) {
+ System.out.println(A.getExclamationMark());
+ }
+ }
+
+ static class A {
+
+ static {
+ System.out.print("Hello world");
+ }
+
+ static String getExclamationMark() {
+ return "!";
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/clinit/ClassInitializedByKeepRuleTest.java b/src/test/java/com/android/tools/r8/shaking/clinit/ClassInitializedByKeepRuleTest.java
new file mode 100644
index 0000000..2e31c07
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/clinit/ClassInitializedByKeepRuleTest.java
@@ -0,0 +1,57 @@
+// 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.shaking.clinit;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+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 ClassInitializedByKeepRuleTest extends TestBase {
+
+ private final TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ public ClassInitializedByKeepRuleTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void test() throws Exception {
+ testForR8(parameters.getBackend())
+ .addInnerClasses(ClassInitializedByKeepRuleTest.class)
+ .addKeepClassRules(TestClass.class)
+ .setMinApi(parameters.getApiLevel())
+ .compile()
+ .inspect(this::inspect);
+ }
+
+ private void inspect(CodeInspector inspector) {
+ // Check that A.<clinit>() is not removed.
+ ClassSubject aClassSubject = inspector.clazz(TestClass.class);
+ assertThat(aClassSubject, isPresent());
+ assertThat(aClassSubject.clinit(), isPresent());
+ }
+
+ static class TestClass {
+
+ static {
+ System.out.print("Hello world");
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/clinit/ClassInitializedByNewInstanceTest.java b/src/test/java/com/android/tools/r8/shaking/clinit/ClassInitializedByNewInstanceTest.java
new file mode 100644
index 0000000..2349839
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/clinit/ClassInitializedByNewInstanceTest.java
@@ -0,0 +1,66 @@
+// 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.shaking.clinit;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+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 ClassInitializedByNewInstanceTest extends TestBase {
+
+ private final TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ public ClassInitializedByNewInstanceTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void test() throws Exception {
+ testForR8(parameters.getBackend())
+ .addInnerClasses(ClassInitializedByNewInstanceTest.class)
+ .addKeepMainRule(TestClass.class)
+ .setMinApi(parameters.getApiLevel())
+ .compile()
+ .inspect(this::inspect)
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutputLines("Hello world!");
+ }
+
+ private void inspect(CodeInspector inspector) {
+ // Check that A.<clinit>() is not removed.
+ ClassSubject aClassSubject = inspector.clazz(A.class);
+ assertThat(aClassSubject, isPresent());
+ assertThat(aClassSubject.clinit(), isPresent());
+ }
+
+ static class TestClass {
+
+ public static void main(String[] args) {
+ new A();
+ }
+ }
+
+ static class A {
+
+ static {
+ System.out.println("Hello world!");
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/clinit/ClassInitializedByStaticGetTest.java b/src/test/java/com/android/tools/r8/shaking/clinit/ClassInitializedByStaticGetTest.java
new file mode 100644
index 0000000..9501a21
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/clinit/ClassInitializedByStaticGetTest.java
@@ -0,0 +1,68 @@
+// 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.shaking.clinit;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+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 ClassInitializedByStaticGetTest extends TestBase {
+
+ private final TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ public ClassInitializedByStaticGetTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void test() throws Exception {
+ testForR8(parameters.getBackend())
+ .addInnerClasses(ClassInitializedByStaticGetTest.class)
+ .addKeepMainRule(TestClass.class)
+ .setMinApi(parameters.getApiLevel())
+ .compile()
+ .inspect(this::inspect)
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutputLines("Hello world!");
+ }
+
+ private void inspect(CodeInspector inspector) {
+ // Check that A.<clinit>() is not removed.
+ ClassSubject aClassSubject = inspector.clazz(A.class);
+ assertThat(aClassSubject, isPresent());
+ assertThat(aClassSubject.clinit(), isPresent());
+ }
+
+ static class TestClass {
+
+ public static void main(String[] args) {
+ System.out.println(A.EXCLAMATION_MARK);
+ }
+ }
+
+ static class A {
+
+ static String EXCLAMATION_MARK = "!";
+
+ static {
+ System.out.print("Hello world");
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/clinit/ClassNotInitializedByCheckCastTest.java b/src/test/java/com/android/tools/r8/shaking/clinit/ClassNotInitializedByCheckCastTest.java
new file mode 100644
index 0000000..0a1914f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/clinit/ClassNotInitializedByCheckCastTest.java
@@ -0,0 +1,69 @@
+// 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.shaking.clinit;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+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 ClassNotInitializedByCheckCastTest extends TestBase {
+
+ private final TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ public ClassNotInitializedByCheckCastTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void test() throws Exception {
+ testForR8(parameters.getBackend())
+ .addInnerClasses(ClassNotInitializedByCheckCastTest.class)
+ .addKeepMainRule(TestClass.class)
+ .setMinApi(parameters.getApiLevel())
+ .compile()
+ .inspect(this::inspect)
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutputLines("null");
+ }
+
+ private void inspect(CodeInspector inspector) {
+ // Check that A.<clinit>() is removed.
+ ClassSubject aClassSubject = inspector.clazz(A.class);
+ assertThat(aClassSubject, isPresent());
+ assertThat(aClassSubject.clinit(), not(isPresent()));
+ }
+
+ static class TestClass {
+
+ static Object object = System.currentTimeMillis() >= 0 ? null : new Object();
+
+ public static void main(String[] args) {
+ System.out.println((A) object);
+ }
+ }
+
+ static class A {
+
+ static {
+ System.out.println("Hello world!");
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/clinit/ClassNotInitializedByConstClassTest.java b/src/test/java/com/android/tools/r8/shaking/clinit/ClassNotInitializedByConstClassTest.java
new file mode 100644
index 0000000..f40cc94
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/clinit/ClassNotInitializedByConstClassTest.java
@@ -0,0 +1,67 @@
+// 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.shaking.clinit;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+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 ClassNotInitializedByConstClassTest extends TestBase {
+
+ private final TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ public ClassNotInitializedByConstClassTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void test() throws Exception {
+ R8TestRunResult result =
+ testForR8(parameters.getBackend())
+ .addInnerClasses(ClassNotInitializedByConstClassTest.class)
+ .addKeepMainRule(TestClass.class)
+ .setMinApi(parameters.getApiLevel())
+ .run(parameters.getRuntime(), TestClass.class);
+
+ // Check that A.<clinit>() is removed.
+ CodeInspector inspector = result.inspector();
+ ClassSubject aClassSubject = inspector.clazz(A.class);
+ assertThat(aClassSubject, isPresent());
+ assertThat(aClassSubject.clinit(), not(isPresent()));
+
+ result.assertSuccessWithOutputLines(aClassSubject.getFinalName());
+ }
+
+ static class TestClass {
+
+ public static void main(String[] args) {
+ System.out.println(A.class.getName());
+ }
+ }
+
+ static class A {
+
+ static {
+ System.out.println("Hello world!");
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/clinit/ClassNotInitializedByInstanceOfTest.java b/src/test/java/com/android/tools/r8/shaking/clinit/ClassNotInitializedByInstanceOfTest.java
new file mode 100644
index 0000000..cc64abf
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/clinit/ClassNotInitializedByInstanceOfTest.java
@@ -0,0 +1,71 @@
+// 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.shaking.clinit;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+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 ClassNotInitializedByInstanceOfTest extends TestBase {
+
+ private final TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ public ClassNotInitializedByInstanceOfTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void test() throws Exception {
+ testForR8(parameters.getBackend())
+ .addInnerClasses(ClassNotInitializedByInstanceOfTest.class)
+ .addKeepMainRule(TestClass.class)
+ .addOptionsModification(
+ options -> options.testing.enableCheckCastAndInstanceOfRemoval = false)
+ .setMinApi(parameters.getApiLevel())
+ .compile()
+ .inspect(this::inspect)
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutputLines("false");
+ }
+
+ private void inspect(CodeInspector inspector) {
+ // Check that A.<clinit>() is removed.
+ ClassSubject aClassSubject = inspector.clazz(A.class);
+ assertThat(aClassSubject, isPresent());
+ assertThat(aClassSubject.clinit(), not(isPresent()));
+ }
+
+ static class TestClass {
+
+ static Object object = System.currentTimeMillis() >= 0 ? new Object() : null;
+
+ public static void main(String[] args) {
+ System.out.println(object instanceof A);
+ }
+ }
+
+ static class A {
+
+ static {
+ System.out.println("Hello world!");
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceInitializedByImplementationTest.java b/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceInitializedByImplementationTest.java
new file mode 100644
index 0000000..3f68961
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceInitializedByImplementationTest.java
@@ -0,0 +1,97 @@
+// 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.shaking.clinit;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+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 InterfaceInitializedByImplementationTest extends TestBase {
+
+ private final TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters()
+ .withAllRuntimes()
+ // Ensure default interface methods are supported.
+ .withApiLevelsStartingAtIncluding(AndroidApiLevel.N)
+ .build();
+ }
+
+ public InterfaceInitializedByImplementationTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void test() throws Exception {
+ testForR8(parameters.getBackend())
+ .addInnerClasses(InterfaceInitializedByImplementationTest.class)
+ .addKeepMainRule(TestClass.class)
+ .enableInliningAnnotations()
+ .setMinApi(parameters.getApiLevel())
+ .noMinification()
+ .compile()
+ .inspect(this::inspect)
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutputLines("Hello world!");
+ }
+
+ private void inspect(CodeInspector inspector) {
+ // Check that A is not removed.
+ ClassSubject aClasssubject = inspector.clazz(A.class);
+ assertThat(aClasssubject, isPresent());
+
+ // Check that I.<clinit>() is not removed.
+ ClassSubject iClassSubject = inspector.clazz(I.class);
+ assertThat(iClassSubject, isPresent());
+ assertThat(iClassSubject.clinit(), isPresent());
+
+ // Check that B.<clinit>() is not removed.
+ ClassSubject bClassSubject = inspector.clazz(B.class);
+ assertThat(bClassSubject, isPresent());
+ assertThat(bClassSubject.clinit(), isPresent());
+ }
+
+ static class TestClass {
+
+ public static void main(String[] args) {
+ new A().m();
+ }
+ }
+
+ interface I {
+
+ B B_INSTANCE = new B();
+
+ // TODO(b/144266257): If tree shaking removes this method, then I.<clinit>() won't be run when
+ // A is being class initialized.
+ @NeverInline
+ default void m() {
+ System.out.println(" world!");
+ }
+ }
+
+ static class A implements I {}
+
+ static class B {
+
+ static {
+ System.out.print("Hello");
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptSingletonIsNotCyclicTest.java b/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptSingletonIsNotCyclicTest.java
index e094a13..ecfc895 100644
--- a/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptSingletonIsNotCyclicTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptSingletonIsNotCyclicTest.java
@@ -92,12 +92,14 @@
QueryNode mainMethod = inspector.method(mainMethodRef).assertNotRenamed().assertKeptBy(root);
// TestClass.<init> is kept by TestClass.main.
QueryNode testInit = inspector.method(testInitRef).assertPresent().assertKeptBy(mainMethod);
- // Foo.<clinit> is kept by TestClass.<init>
- QueryNode fooClInit = inspector.method(fooClInitRef).assertPresent().assertKeptBy(testInit);
+ // The type Foo is kept by TestClass.<init>
+ QueryNode fooClassNode = inspector.clazz(fooClassRef).assertRenamed().assertKeptBy(testInit);
+ // Foo.<clinit> is kept by Foo
+ QueryNode fooClInit = inspector.method(fooClInitRef).assertPresent().assertKeptBy(fooClassNode);
+ // The type Foo is also kept by Foo.<clinit>
+ fooClassNode.assertKeptBy(fooClInit);
// Foo.<init> is kept by Foo.<clinit>
QueryNode fooInit = inspector.method(fooInitRef).assertPresent().assertKeptBy(fooClInit);
- // The type Foo is kept by the class constructor of Foo and the instance initializer.
- inspector.clazz(fooClassRef).assertRenamed().assertKeptBy(fooClInit).assertKeptBy(fooInit);
}
public static final class FooStaticMethod {
diff --git a/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptViaClassInitializerTestRunner.java b/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptViaClassInitializerTestRunner.java
index 3c5c230..707a415 100644
--- a/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptViaClassInitializerTestRunner.java
+++ b/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptViaClassInitializerTestRunner.java
@@ -14,6 +14,8 @@
import com.android.tools.r8.NeverClassInline;
import com.android.tools.r8.NeverMerge;
import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.ToolHelper.DexVm.Version;
import com.android.tools.r8.origin.Origin;
@@ -70,25 +72,31 @@
private static final String CLASS_NAME = KeptViaClassInitializerTestRunner.class.getTypeName();
private static final String EXPECTED = StringUtils.lines("I'm an A");
- private final Backend backend;
+ private final TestParameters parameters;
@Parameters(name = "{0}")
- public static Backend[] data() {
- return ToolHelper.getBackends();
+ public static TestParametersCollection data() {
+ return getTestParameters()
+ .withCfRuntimes()
+ .withDexRuntimesStartingFromIncluding(Version.V7_0_0)
+ .withAllApiLevels()
+ .build();
}
- public KeptViaClassInitializerTestRunner(Backend backend) {
- this.backend = backend;
+ public KeptViaClassInitializerTestRunner(TestParameters parameters) {
+ this.parameters = parameters;
}
- public static String KEPT_REASON_SUFFIX = StringUtils.lines(
- // The full reason is not shared between CF and DEX due to desugaring.
- "| void " + CLASS_NAME + "$T.<clinit>()",
- "|- is referenced from:",
- "| void " + CLASS_NAME + "$Main.main(java.lang.String[])",
- "|- is referenced in keep rule:",
- "| -keep class " + CLASS_NAME + "$Main { void main(java.lang.String[]); }"
- );
+ public static String KEPT_REASON_SUFFIX =
+ StringUtils.lines(
+ // The full reason is not shared between CF and DEX due to desugaring.
+ "| void " + CLASS_NAME + "$T.<clinit>()",
+ "|- is reachable from:",
+ "| com.android.tools.r8.shaking.keptgraph.KeptViaClassInitializerTestRunner$T",
+ "|- is referenced from:",
+ "| void " + CLASS_NAME + "$Main.main(java.lang.String[])",
+ "|- is referenced in keep rule:",
+ "| -keep class " + CLASS_NAME + "$Main { void main(java.lang.String[]); }");
@Test
public void testKeptMethod() throws Exception {
@@ -99,18 +107,18 @@
WhyAreYouKeepingConsumer consumer = new WhyAreYouKeepingConsumer(null);
GraphInspector inspector =
- testForR8(backend)
+ testForR8(parameters.getBackend())
.enableGraphInspector(consumer)
.addProgramClassesAndInnerClasses(Main.class, A.class, T.class)
.addKeepMethodRules(mainMethod)
.setMinApi(AndroidApiLevel.N)
.apply(
b -> {
- if (backend == Backend.DEX) {
+ if (parameters.isDexRuntime()) {
b.addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.N));
}
})
- .run(Main.class)
+ .run(parameters.getRuntime(), Main.class)
.assertSuccessWithOutput(EXPECTED)
.graphInspector();
@@ -124,7 +132,7 @@
// TODO(b/124499108): Currently synthetic lambda classes are referenced,
// should be their originating context.
- if (backend == Backend.DEX) {
+ if (parameters.isDexRuntime()) {
assertThat(baos.toString(), containsString("-$$Lambda$"));
} else {
assertThat(baos.toString(), not(containsString("-$$Lambda$")));