Record static edge from the context of static invokes to the static receiver.

This deals with one of the two issues in b/135474075, where a singleton pattern
(the type Absent) was reported to have only a cyclic reason for being kept. A
regression test is added for the remaining issue: the super type (the type
Optional in the report) is reported as not being kept despite being in the
output.

Bug: 124501298
Bug: 135474075
Change-Id: I7c4f08e8f7a16e364b72b577a2c8154ad97f82a8
diff --git a/src/main/java/com/android/tools/r8/references/Reference.java b/src/main/java/com/android/tools/r8/references/Reference.java
index decd262..8a3d278 100644
--- a/src/main/java/com/android/tools/r8/references/Reference.java
+++ b/src/main/java/com/android/tools/r8/references/Reference.java
@@ -10,6 +10,7 @@
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.ConcurrentMap;
 
@@ -162,6 +163,10 @@
     return method(classFromClass(holderClass), "<init>", builder.build(), null);
   }
 
+  public static MethodReference classConstructor(ClassReference type) {
+    return method(type, "<clinit>", Collections.emptyList(), null);
+  }
+
   /** Get a field reference from its full reference specification. */
   public static FieldReference field(
       ClassReference holderClass, String fieldName, TypeReference fieldType) {
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 175a41d..093adaa 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -1080,10 +1080,18 @@
     // Only mark methods for which invocation will succeed at runtime live.
     DexEncodedMethod targetMethod = appInfo.dispatchStaticInvoke(resolutionResult);
     if (targetMethod != null) {
+      registerClassInitializer(targetMethod.method.holder, reason);
       markDirectStaticOrConstructorMethodAsLive(targetMethod, reason);
     }
   }
 
+  private void registerClassInitializer(DexType holder, KeepReason reason) {
+    DexClass definition = appView.definitionFor(holder);
+    if (definition != null && definition.hasClassInitializer()) {
+      registerMethod(definition.getClassInitializer(), reason);
+    }
+  }
+
   private void handleInvokeOfDirectTarget(DexMethod method, KeepReason reason) {
     // We have to mark the resolved method as targeted even if it cannot actually be invoked
     // to make sure the invocation will keep failing in the appropriate way.
@@ -1337,6 +1345,7 @@
   private void markStaticFieldAsLive(
       DexField field, KeepReason reason, DexEncodedField encodedField) {
     // Mark the type live here, so that the class exists at runtime.
+    registerClassInitializer(field.holder, reason);
     markTypeAsLive(field.holder);
     markTypeAsLive(field.type);
 
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
new file mode 100644
index 0000000..8ff2539
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptSingletonIsNotCyclicTest.java
@@ -0,0 +1,158 @@
+// 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.keptgraph;
+
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersBuilder;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.shaking.WhyAreYouKeepingConsumer;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.graphinspector.GraphInspector;
+import com.android.tools.r8.utils.graphinspector.GraphInspector.QueryNode;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+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 KeptSingletonIsNotCyclicTest extends TestBase {
+
+  private static final String EXPECTED = StringUtils.lines("Foo!");
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return TestParametersBuilder.builder().withCfRuntimes().build();
+  }
+
+  public KeptSingletonIsNotCyclicTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testStaticMethod() throws Exception {
+    test(FooStaticMethod.class, TestStaticMethod.class);
+  }
+
+  @Test
+  public void testStaticField() throws Exception {
+    test(FooStaticField.class, TestStaticField.class);
+  }
+
+  private void test(Class<?> fooClass, Class<?> testClass) throws Exception {
+    WhyAreYouKeepingConsumer whyAreYouKeepingConsumer = new WhyAreYouKeepingConsumer(null);
+    GraphInspector inspector =
+        testForR8(parameters.getBackend())
+            .enableClassInliningAnnotations()
+            .enableGraphInspector(whyAreYouKeepingConsumer)
+            .addProgramClasses(testClass, fooClass)
+            .addKeepMainRule(testClass)
+            .run(parameters.getRuntime(), testClass)
+            .assertSuccessWithOutput(EXPECTED)
+            .graphInspector();
+
+    ClassReference fooClassRef = Reference.classFromClass(fooClass);
+    MethodReference fooClInitRef = Reference.classConstructor(fooClassRef);
+    MethodReference fooInitRef = Reference.methodFromMethod(fooClass.getDeclaredConstructor());
+
+    MethodReference testInitRef =
+        Reference.methodFromMethod(testClass.getDeclaredConstructor());
+
+    MethodReference mainMethodRef =
+        Reference.methodFromMethod(testClass.getDeclaredMethod("main", String[].class));
+
+    // Check that whyareyoukeeping does not mention cycles.
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    whyAreYouKeepingConsumer.printWhyAreYouKeeping(fooClassRef, new PrintStream(baos));
+    assertThat(
+        baos.toString().replace(getClass().getTypeName(), "<test>").toLowerCase(),
+        not(anyOf(containsString("cyclic"), containsString("cycle"))));
+
+    // The only root should be the keep main-method rule.
+    assertEquals(1, inspector.getRoots().size());
+    QueryNode root = inspector.rule(Origin.unknown(), 1, 1).assertRoot();
+
+    // TestClass.main is kept by the root rule.
+    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);
+    // Foo.<init> is kept by Foo.<clinit>
+    QueryNode fooInit = inspector.method(fooInitRef).assertPresent().assertKeptBy(fooClInit);
+    // TODO(b/139273002): It should be the class constructor of Foo that mark Foo as instantiated.
+    inspector.clazz(fooClassRef).assertRenamed().assertKeptBy(fooClInit);
+    assertFalse(inspector.clazz(fooClassRef).assertRenamed().isKeptBy(fooInit));
+  }
+
+  public static final class FooStaticMethod {
+
+    static final FooStaticMethod INSTANCE = new FooStaticMethod();
+
+    public static FooStaticMethod getInstance() {
+      return INSTANCE;
+    }
+
+    private FooStaticMethod() {}
+
+    @Override
+    public String toString() {
+      return "Foo!";
+    }
+  }
+
+  @NeverClassInline
+  public static class TestStaticMethod {
+    public FooStaticMethod foo;
+
+    public TestStaticMethod() {
+      this.foo = FooStaticMethod.getInstance();
+    }
+
+    public static void main(String[] args) {
+      System.out.println(new TestStaticMethod().foo.toString());
+    }
+  }
+
+  public static final class FooStaticField {
+
+    static final FooStaticField INSTANCE = new FooStaticField();
+
+    private FooStaticField() {}
+
+    @Override
+    public String toString() {
+      return "Foo!";
+    }
+  }
+
+  @NeverClassInline
+  public static class TestStaticField {
+    public FooStaticField foo;
+
+    public TestStaticField() {
+      this.foo = FooStaticField.INSTANCE;
+    }
+
+    public static void main(String[] args) {
+      System.out.println(new TestStaticField().foo.toString());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptSubclassKeepsSuperTest.java b/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptSubclassKeepsSuperTest.java
new file mode 100644
index 0000000..6bc7cb5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptSubclassKeepsSuperTest.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.keptgraph;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverMerge;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersBuilder;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.graphinspector.GraphInspector;
+import com.android.tools.r8.utils.graphinspector.GraphInspector.QueryNode;
+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 KeptSubclassKeepsSuperTest extends TestBase {
+
+  private static final Class<?> CLASS = TestClass.class;
+  private static final String EXPECTED = StringUtils.lines("Foo!");
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return TestParametersBuilder.builder().withCfRuntimes().build();
+  }
+
+  public KeptSubclassKeepsSuperTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    GraphInspector inspector =
+        testForR8(parameters.getBackend())
+            .enableGraphInspector()
+            .enableMergeAnnotations()
+            .addProgramClasses(CLASS, Foo.class, Bar.class)
+            .addKeepMainRule(CLASS)
+            .noMinification()
+            .run(parameters.getRuntime(), CLASS)
+            .assertSuccessWithOutput(EXPECTED)
+            .graphInspector();
+
+    // The only root should be the keep main-method rule.
+    assertEquals(1, inspector.getRoots().size());
+    inspector.rule(Origin.unknown(), 1, 1).assertRoot();
+
+    QueryNode fooClass = inspector.clazz(Reference.classFromClass(Foo.class));
+    fooClass.assertPresent();
+
+    ClassReference barClassRef = Reference.classFromClass(Bar.class);
+    QueryNode barClass = inspector.clazz(barClassRef);
+    // TODO(b/135474075): No edge is reported for the super class!
+    barClass.assertAbsent();
+  }
+
+  @NeverMerge
+  public abstract static class Bar {}
+
+  public static final class Foo extends Bar {
+
+    static final Foo INSTANCE = new Foo();
+
+    public static Foo getInstance() {
+      return INSTANCE;
+    }
+
+    private Foo() {}
+
+    @Override
+    public String toString() {
+      return "Foo!";
+    }
+  }
+
+  public static class TestClass {
+    public Bar bar;
+
+    public TestClass() {
+      this.bar = Foo.getInstance();
+    }
+
+    public static void main(String[] args) {
+      System.out.println(new TestClass().bar.toString());
+    }
+  }
+}
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 ddf82f0..3c5c230 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
@@ -67,6 +67,7 @@
     }
   }
 
+  private static final String CLASS_NAME = KeptViaClassInitializerTestRunner.class.getTypeName();
   private static final String EXPECTED = StringUtils.lines("I'm an A");
 
   private final Backend backend;
@@ -80,6 +81,15 @@
     this.backend = backend;
   }
 
+  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[]); }"
+  );
+
   @Test
   public void testKeptMethod() throws Exception {
     assumeTrue(ToolHelper.getDexVm().getVersion().isAtLeast(Version.V7_0_0));
@@ -110,9 +120,7 @@
 
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
     consumer.printWhyAreYouKeeping(classFromClass(A.class), new PrintStream(baos));
-
-    // TODO(b/124501298): Currently the rooted path is not found.
-    assertThat(baos.toString(), containsString("is kept for unknown reason"));
+    assertThat(baos.toString(), containsString(KEPT_REASON_SUFFIX));
 
     // TODO(b/124499108): Currently synthetic lambda classes are referenced,
     //  should be their originating context.
diff --git a/src/test/java/com/android/tools/r8/shaking/keptgraph/WhyAreYouKeepingAllTest.java b/src/test/java/com/android/tools/r8/shaking/keptgraph/WhyAreYouKeepingAllTest.java
index da94e04..4f0ba9e 100644
--- a/src/test/java/com/android/tools/r8/shaking/keptgraph/WhyAreYouKeepingAllTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/keptgraph/WhyAreYouKeepingAllTest.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.shaking.keptgraph;
 
 import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.TestBase;
@@ -38,7 +39,10 @@
         .redirectStdOut(new PrintStream(baos))
         .compile();
     assertThat(baos.toString(), containsString("referenced in keep rule"));
+
     // TODO(b/124655065): We should always know the reason for keeping.
-    assertThat(baos.toString(), containsString("kept for unknown reasons"));
+    // It is OK if this starts failing while the kept-graph API is incomplete, in which case replace
+    // the 'not(containsString(' by just 'containsString('.
+    assertThat(baos.toString(), not(containsString("kept for unknown reasons")));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/utils/graphinspector/GraphInspector.java b/src/test/java/com/android/tools/r8/utils/graphinspector/GraphInspector.java
index 3d155db..4729766 100644
--- a/src/test/java/com/android/tools/r8/utils/graphinspector/GraphInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/graphinspector/GraphInspector.java
@@ -27,10 +27,10 @@
 import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.shaking.CollectingGraphConsumer;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableSet;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.IdentityHashMap;
-import java.util.Iterator;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
@@ -225,6 +225,8 @@
           isSatisfiedBy(node));
       return this;
     }
+
+    public abstract String getKeptGraphString();
   }
 
   private static class AbsentQueryNode extends QueryNode {
@@ -236,6 +238,11 @@
     }
 
     @Override
+    public String getKeptGraphString() {
+      return "<not kept>";
+    }
+
+    @Override
     public boolean equals(Object obj) {
       return obj instanceof AbsentQueryNode
           && failedQueryNodeDescription.equals(((AbsentQueryNode) obj).failedQueryNodeDescription);
@@ -393,6 +400,39 @@
       return filterSources((source, infos) -> impl.graphNode == source).findFirst().isPresent();
     }
 
+    @Override
+    public String getKeptGraphString() {
+      StringBuilder builder = new StringBuilder();
+      getKeptGraphString(graphNode, inspector, builder, "", ImmutableSet.of());
+      return builder.toString();
+    }
+
+    private static void getKeptGraphString(
+        GraphNode graphNode,
+        GraphInspector inspector,
+        StringBuilder builder,
+        String indent,
+        Set<GraphNode> seen) {
+      builder.append(graphNode);
+      if (seen.contains(graphNode)) {
+        builder.append(" <CYCLE>");
+        return;
+      }
+      seen = ImmutableSet.<GraphNode>builder().addAll(seen).add(graphNode).build();
+      Map<GraphNode, Set<GraphEdgeInfo>> sources =
+          inspector.consumer.getSourcesTargeting(graphNode);
+      if (sources == null) {
+        builder.append(" <ROOT>");
+        return;
+      }
+      for (Entry<GraphNode, Set<GraphEdgeInfo>> entry : sources.entrySet()) {
+        GraphNode source = entry.getKey();
+        Set<GraphEdgeInfo> reasons = entry.getValue();
+        builder.append('\n').append(indent).append("<- ");
+        getKeptGraphString(source, inspector, builder, indent + "  ", seen);
+      }
+    }
+
     private Stream<GraphNode> filterSources(BiPredicate<GraphNode, Set<GraphEdgeInfo>> test) {
       Map<GraphNode, Set<GraphEdgeInfo>> sources =
           inspector.consumer.getSourcesTargeting(graphNode);