Version 1.4.54

Cherry-pick: Ensure that use of -whyareyoukeeping does not cause compilation to fail.
CL: https://r8-review.googlesource.com/c/r8/+/34541

Cherry-pick: Allow functions with throwing clauses in testing map and apply.
CL: https://r8-review.googlesource.com/c/r8/+/34533

Cherry-pick: Add map and apply to the testing builders.
CL: https://r8-review.googlesource.com/c/r8/+/34473

Bug: 124655989
Change-Id: I063fb05fc18956efb9aae80ad1f061fef2e28839
diff --git a/src/main/java/com/android/tools/r8/Version.java b/src/main/java/com/android/tools/r8/Version.java
index 1f49155..543f16a 100644
--- a/src/main/java/com/android/tools/r8/Version.java
+++ b/src/main/java/com/android/tools/r8/Version.java
@@ -11,7 +11,7 @@
 
   // This field is accessed from release scripts using simple pattern matching.
   // Therefore, changing this field could break our release scripts.
-  public static final String LABEL = "1.4.53";
+  public static final String LABEL = "1.4.54";
 
   private Version() {
   }
diff --git a/src/main/java/com/android/tools/r8/experimental/graphinfo/GraphEdgeInfo.java b/src/main/java/com/android/tools/r8/experimental/graphinfo/GraphEdgeInfo.java
index ca3a732..3e55242 100644
--- a/src/main/java/com/android/tools/r8/experimental/graphinfo/GraphEdgeInfo.java
+++ b/src/main/java/com/android/tools/r8/experimental/graphinfo/GraphEdgeInfo.java
@@ -3,10 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.experimental.graphinfo;
 
-import com.android.tools.r8.errors.Unreachable;
-
 public class GraphEdgeInfo {
 
+  private static GraphEdgeInfo UNKNOWN = new GraphEdgeInfo(EdgeKind.Unknown);
+
+  public static GraphEdgeInfo unknown() {
+    return UNKNOWN;
+  }
+
   // TODO(b/120959039): Simplify these. Most of the information is present in the source node.
   public enum EdgeKind {
     // Prioritized list of edge types.
@@ -23,7 +27,8 @@
     ReachableFromLiveType,
     ReferencedInAnnotation,
     IsLibraryMethod,
-    MethodHandleUseFrom
+    MethodHandleUseFrom,
+    Unknown
   }
 
   private final EdgeKind kind;
@@ -66,7 +71,10 @@
       case MethodHandleUseFrom:
         return "referenced by method handle";
       default:
-        throw new Unreachable("Unexpected edge kind: " + edgeKind());
+        assert false : "Unknown edge kind: " + edgeKind();
+        // fall through
+      case Unknown:
+        return "kept for unknown reasons";
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/experimental/graphinfo/GraphNode.java b/src/main/java/com/android/tools/r8/experimental/graphinfo/GraphNode.java
index b99485a..12661c0 100644
--- a/src/main/java/com/android/tools/r8/experimental/graphinfo/GraphNode.java
+++ b/src/main/java/com/android/tools/r8/experimental/graphinfo/GraphNode.java
@@ -8,12 +8,38 @@
 @Keep
 public abstract class GraphNode {
 
+  private static final GraphNode CYCLE =
+      new GraphNode(false) {
+        @Override
+        public boolean equals(Object o) {
+          return o == this;
+        }
+
+        @Override
+        public int hashCode() {
+          return 0;
+        }
+
+        @Override
+        public String toString() {
+          return "cycle";
+        }
+      };
+
   private final boolean isLibraryNode;
 
   public GraphNode(boolean isLibraryNode) {
     this.isLibraryNode = isLibraryNode;
   }
 
+  public static GraphNode cycle() {
+    return CYCLE;
+  }
+
+  public final boolean isCycle() {
+    return this == cycle();
+  }
+
   public boolean isLibraryNode() {
     return isLibraryNode;
   }
diff --git a/src/main/java/com/android/tools/r8/shaking/WhyAreYouKeepingConsumer.java b/src/main/java/com/android/tools/r8/shaking/WhyAreYouKeepingConsumer.java
index 89a6f0e..d56e455 100644
--- a/src/main/java/com/android/tools/r8/shaking/WhyAreYouKeepingConsumer.java
+++ b/src/main/java/com/android/tools/r8/shaking/WhyAreYouKeepingConsumer.java
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.shaking;
 
-import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.experimental.graphinfo.ClassGraphNode;
 import com.android.tools.r8.experimental.graphinfo.FieldGraphNode;
 import com.android.tools.r8.experimental.graphinfo.GraphConsumer;
@@ -32,6 +31,7 @@
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -125,50 +125,53 @@
     out.println(DescriptorUtils.descriptorToJavaType(clazz.getDescriptor()));
   }
 
-  public List<Pair<GraphNode, GraphEdgeInfo>> findShortestPathTo(final GraphNode node) {
+  private List<Pair<GraphNode, GraphEdgeInfo>> findShortestPathTo(final GraphNode node) {
     if (node == null) {
       return null;
     }
-    Deque<GraphPath> queue;
-    {
-      Map<GraphNode, Set<GraphEdgeInfo>> sources = getSourcesTargeting(node);
-      if (sources == null) {
-        // The node is not targeted at all (it is not reachable).
-        return null;
-      }
-      queue = new LinkedList<>();
-      for (GraphNode source : sources.keySet()) {
-        queue.addLast(new GraphPath(source, null));
-      }
-    }
     Map<GraphNode, GraphNode> seen = new IdentityHashMap<>();
-    while (!queue.isEmpty()) {
-      GraphPath path = queue.removeFirst();
-      Map<GraphNode, Set<GraphEdgeInfo>> sources = getSourcesTargeting(path.node);
+    Deque<GraphPath> queue = new LinkedList<>();
+    GraphPath path = null;
+    GraphNode current = node;
+    while (true) {
+      Map<GraphNode, Set<GraphEdgeInfo>> sources = getSourcesTargeting(current);
       if (sources == null) {
+        // We have reached a root or the current node is not targeted at all.
         return getCanonicalPath(path, node);
       }
+      assert !sources.isEmpty();
       for (GraphNode source : sources.keySet()) {
-        if (seen.containsKey(source)) {
-          continue;
+        if (!seen.containsKey(source)) {
+          seen.put(source, source);
+          queue.addLast(new GraphPath(source, path));
         }
-        seen.put(source, source);
-        queue.addLast(new GraphPath(source, path));
       }
+      // The source set was not empty, thus we don't have a real root, but all sources are seen!
+      if (queue.isEmpty()) {
+        return getCanonicalPath(new GraphPath(GraphNode.cycle(), path), node);
+      }
+      path = queue.removeFirst();
+      current = path.node;
     }
-    throw new Unreachable("Failed to find a root from node: " + node);
   }
 
   // Convert a internal path representation to the external API and compute the edge reasons.
   private List<Pair<GraphNode, GraphEdgeInfo>> getCanonicalPath(
       GraphPath path, GraphNode endTarget) {
-    assert path != null;
+    if (path == null) {
+      // If there is no path to endTarget, treat it as not kept by returning a null path.
+      return null;
+    }
     List<Pair<GraphNode, GraphEdgeInfo>> canonical = new ArrayList<>();
     while (path.path != null) {
       GraphNode source = path.node;
-      GraphNode target = path.path.node;
-      Set<GraphEdgeInfo> infos = getSourcesTargeting(target).get(source);
-      canonical.add(new Pair<>(source, getCanonicalInfo(infos)));
+      if (source.isCycle()) {
+        canonical.add(new Pair<>(source, new GraphEdgeInfo(EdgeKind.Unknown)));
+      } else {
+        GraphNode target = path.path.node;
+        Set<GraphEdgeInfo> infos = getSourcesTargeting(target).get(source);
+        canonical.add(new Pair<>(source, getCanonicalInfo(infos)));
+      }
       path = path.path;
     }
     Set<GraphEdgeInfo> infos = getSourcesTargeting(endTarget).get(path.node);
@@ -186,7 +189,8 @@
         }
       }
     }
-    throw new Unreachable("Unexpected empty set of graph edge info");
+    assert false : "Unexpected empty set of graph edge info";
+    return GraphEdgeInfo.unknown();
   }
 
   private void printEdge(GraphNode node, GraphEdgeInfo info, Formatter formatter) {
@@ -225,7 +229,11 @@
           ? keepRuleNode.getContent()
           : keepRuleNode.getOrigin() + ":" + shortPositionInfo(keepRuleNode.getPosition());
     }
-    throw new Unreachable("Unexpected graph node type: " + node);
+    if (node == GraphNode.cycle()) {
+      return "only cyclic dependencies remain, failed to determine a path from a keep rule";
+    }
+    assert false : "Unexpected graph node type: " + node;
+    return Objects.toString(node);
   }
 
   private void addNodeMessage(GraphNode node, Formatter formatter) {
diff --git a/src/test/java/com/android/tools/r8/TestBaseResult.java b/src/test/java/com/android/tools/r8/TestBaseResult.java
index e7623a3..11b7bd3 100644
--- a/src/test/java/com/android/tools/r8/TestBaseResult.java
+++ b/src/test/java/com/android/tools/r8/TestBaseResult.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8;
 
 public abstract class TestBaseResult<CR extends TestBaseResult<CR, RR>, RR extends TestRunResult> {
+
   final TestState state;
 
   TestBaseResult(TestState state) {
@@ -12,4 +13,13 @@
   }
 
   public abstract CR self();
+
+  public <S> S map(ThrowableFunction<CR, S> fn) {
+    return fn.applyWithRuntimeException(self());
+  }
+
+  public <T extends Throwable> CR apply(ThrowableConsumer<CR> fn) {
+    fn.acceptWithRuntimeException(self());
+    return self();
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/TestBuilder.java b/src/test/java/com/android/tools/r8/TestBuilder.java
index c67c05c..cee2519 100644
--- a/src/test/java/com/android/tools/r8/TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestBuilder.java
@@ -24,6 +24,15 @@
 
   abstract T self();
 
+  public <S> S map(ThrowableFunction<T, S> fn) {
+    return fn.applyWithRuntimeException(self());
+  }
+
+  public T apply(ThrowableConsumer<T> fn) {
+    fn.acceptWithRuntimeException(self());
+    return self();
+  }
+
   public abstract RR run(String mainClass)
       throws IOException, CompilationFailedException;
 
diff --git a/src/test/java/com/android/tools/r8/TestRunResult.java b/src/test/java/com/android/tools/r8/TestRunResult.java
index 65a57e6..b752bce 100644
--- a/src/test/java/com/android/tools/r8/TestRunResult.java
+++ b/src/test/java/com/android/tools/r8/TestRunResult.java
@@ -9,12 +9,12 @@
 import static org.junit.Assert.assertThat;
 
 import com.android.tools.r8.ToolHelper.ProcessResult;
-import com.android.tools.r8.graph.invokesuper.Consumer;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import java.io.IOException;
 import java.io.PrintStream;
 import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
 import java.util.function.Function;
 import org.hamcrest.Matcher;
 
@@ -29,6 +29,19 @@
 
   abstract RR self();
 
+  public <S> S map(Function<RR, S> fn) {
+    return fn.apply(self());
+  }
+
+  public RR apply(Consumer<RR> fn) {
+    fn.accept(self());
+    return self();
+  }
+
+  public AndroidApp app() {
+    return app;
+  }
+
   public String getStdOut() {
     return result.stdout;
   }
@@ -70,10 +83,6 @@
     return self();
   }
 
-  public <R> R map(Function<RR, R> mapper) {
-    return mapper.apply(self());
-  }
-
   public CodeInspector inspector() throws IOException, ExecutionException {
     // Inspection post run implies success. If inspection of an invalid program is needed it should
     // be done on the compilation result or on the input.
diff --git a/src/test/java/com/android/tools/r8/ThrowableConsumer.java b/src/test/java/com/android/tools/r8/ThrowableConsumer.java
new file mode 100644
index 0000000..664238e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ThrowableConsumer.java
@@ -0,0 +1,23 @@
+// 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;
+
+import java.util.function.Function;
+
+public interface ThrowableConsumer<Formal> {
+  void accept(Formal formal) throws Throwable;
+
+  default <T extends Throwable> void acceptWithHandler(
+      Formal formal, Function<Throwable, T> handler) throws T {
+    try {
+      accept(formal);
+    } catch (Throwable e) {
+      throw handler.apply(e);
+    }
+  }
+
+  default void acceptWithRuntimeException(Formal formal) {
+    acceptWithHandler(formal, RuntimeException::new);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ThrowableFunction.java b/src/test/java/com/android/tools/r8/ThrowableFunction.java
new file mode 100644
index 0000000..b97d42b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ThrowableFunction.java
@@ -0,0 +1,23 @@
+// 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;
+
+import java.util.function.Function;
+
+public interface ThrowableFunction<Formal, Return> {
+  Return apply(Formal formal) throws Throwable;
+
+  default <T extends Throwable> Return applyWithHandler(
+      Formal formal, Function<Throwable, T> handler) throws T {
+    try {
+      return apply(formal);
+    } catch (Throwable e) {
+      throw handler.apply(e);
+    }
+  }
+
+  default Return applyWithRuntimeException(Formal formal) {
+    return applyWithHandler(formal, RuntimeException::new);
+  }
+}
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
new file mode 100644
index 0000000..344721f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptViaClassInitializerTestRunner.java
@@ -0,0 +1,126 @@
+// 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 com.android.tools.r8.references.Reference.classFromClass;
+import static com.android.tools.r8.references.Reference.methodFromMethod;
+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 com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverMerge;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.VmTestRunner.IgnoreIfVmOlderThan;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.shaking.WhyAreYouKeepingConsumer;
+import com.android.tools.r8.utils.AndroidApiLevel;
+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 java.util.function.Supplier;
+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 KeptViaClassInitializerTestRunner extends TestBase {
+
+  @NeverMerge
+  @NeverClassInline
+  public static class A {
+
+    @Override
+    public String toString() {
+      return "I'm an A";
+    }
+  }
+
+  @NeverMerge
+  @NeverClassInline
+  public enum T {
+    A(A::new);
+
+    private final Supplier<Object> factory;
+
+    T(Supplier<Object> factory) {
+      this.factory = factory;
+    }
+
+    public Object create() {
+      return factory.get();
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(T.A.create());
+    }
+  }
+
+  private static final Class<?> CLASS = Main.class;
+  private static final String EXPECTED = StringUtils.lines("I'm an A");
+
+  private final Backend backend;
+
+  @Parameters(name = "{0}")
+  public static Backend[] data() {
+    return Backend.values();
+  }
+
+  public KeptViaClassInitializerTestRunner(Backend backend) {
+    this.backend = backend;
+  }
+
+  @Test
+  @IgnoreIfVmOlderThan(Version.V7_0_0)
+  public void testKeptMethod() throws Exception {
+    MethodReference mainMethod =
+        methodFromMethod(Main.class.getDeclaredMethod("main", String[].class));
+
+    WhyAreYouKeepingConsumer consumer = new WhyAreYouKeepingConsumer(null);
+    GraphInspector inspector =
+        testForR8(backend)
+            .enableGraphInspector(consumer)
+            .enableInliningAnnotations()
+            .addProgramClassesAndInnerClasses(Main.class, A.class, T.class)
+            .addKeepMethodRules(mainMethod)
+            .apply(
+                b -> {
+                  if (backend == Backend.DEX) {
+                    b.setMinApi(AndroidApiLevel.N);
+                    b.addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.N));
+                  }
+                })
+            .run(Main.class)
+            .assertSuccessWithOutput(EXPECTED)
+            .graphInspector();
+
+    // The only root should be the keep main-method rule.
+    assertEquals(1, inspector.getRoots().size());
+    QueryNode root = inspector.rule(Origin.unknown(), 1, 1).assertRoot();
+
+    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"));
+
+    // TODO(b/124499108): Currently synthetic lambda classes are referenced,
+    // should be their originating context.
+    if (backend == Backend.DEX) {
+      assertThat(baos.toString(), containsString("-$$Lambda$"));
+    } else {
+      assertThat(baos.toString(), not(containsString("-$$Lambda$")));
+    }
+  }
+}
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
new file mode 100644
index 0000000..da94e04
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/keptgraph/WhyAreYouKeepingAllTest.java
@@ -0,0 +1,44 @@
+// 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.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.utils.StringUtils;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.junit.Test;
+
+/**
+ * Run compiling R8 with R8 using a match-all -whyareyoukeeping rule to check that it does not cause
+ * compilation to fail.
+ */
+public class WhyAreYouKeepingAllTest extends TestBase {
+
+  private static final Path MAIN_KEEP = Paths.get("src/main/keep.txt");
+
+  private static final String WHY_ARE_YOU_KEEPING_ALL = StringUtils.lines(
+      "-whyareyoukeeping class ** { *; }",
+      "-whyareyoukeeping @interface ** { *; }"
+  );
+
+  @Test
+  public void test() throws Throwable {
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    testForR8(Backend.CF)
+        .addProgramFiles(ToolHelper.R8_WITH_RELOCATED_DEPS_JAR)
+        .addKeepRuleFiles(MAIN_KEEP)
+        .addKeepRules(WHY_ARE_YOU_KEEPING_ALL)
+        .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"));
+  }
+}