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$")));