Initial API for obtaining the dependency graph for desugaring.

Bug: 138988172
Change-Id: I26d31a1d39ef1d62513242651971d73014a8aa79
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 717a26f..4925c20 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -64,6 +64,7 @@
   public static class Builder extends BaseCompilerCommand.Builder<D8Command, Builder> {
 
     private boolean intermediate = false;
+    private DesugarGraphConsumer desugarGraphConsumer = null;
 
     private Builder() {
       this(new DefaultD8DiagnosticsHandler());
@@ -112,6 +113,21 @@
       return self();
     }
 
+    /** Get the consumer that will receive dependency information for desugaring. */
+    public DesugarGraphConsumer getDesugarGraphConsumer() {
+      return desugarGraphConsumer;
+    }
+
+    /**
+     * Set the consumer that will receive dependency information for desugaring.
+     *
+     * <p>Setting the consumer will clear any previously set consumer.
+     */
+    public Builder setDesugarGraphConsumer(DesugarGraphConsumer desugarGraphConsumer) {
+      this.desugarGraphConsumer = desugarGraphConsumer;
+      return self();
+    }
+
     @Override
     Builder self() {
       return this;
@@ -173,13 +189,15 @@
           isOptimizeMultidexForLinearAlloc(),
           getSpecialLibraryConfiguration(),
           getIncludeClassesChecksum(),
-          getDexClassChecksumFilter());
+          getDexClassChecksumFilter(),
+          getDesugarGraphConsumer());
     }
   }
 
   static final String USAGE_MESSAGE = D8CommandParser.USAGE_MESSAGE;
 
-  private boolean intermediate = false;
+  private final boolean intermediate;
+  private final DesugarGraphConsumer desugarGraphConsumer;
 
   public static Builder builder() {
     return new Builder();
@@ -233,7 +251,8 @@
       boolean optimizeMultidexForLinearAlloc,
       String specialLibraryConfiguration,
       boolean encodeChecksum,
-      BiPredicate<String, Long> dexClassChecksumFilter) {
+      BiPredicate<String, Long> dexClassChecksumFilter,
+      DesugarGraphConsumer desugarGraphConsumer) {
     super(
         inputApp,
         mode,
@@ -247,10 +266,13 @@
         encodeChecksum,
         dexClassChecksumFilter);
     this.intermediate = intermediate;
+    this.desugarGraphConsumer = desugarGraphConsumer;
   }
 
   private D8Command(boolean printHelp, boolean printVersion) {
     super(printHelp, printVersion);
+    intermediate = false;
+    desugarGraphConsumer = null;
   }
 
   private void configureLibraryDesugaring(InternalOptions options) {
@@ -268,6 +290,7 @@
     internal.minApiLevel = getMinApiLevel();
     internal.intermediate = intermediate;
     internal.readCompileTimeAnnotations = intermediate;
+    internal.desugarGraphConsumer = desugarGraphConsumer;
 
     // Assert and fixup defaults.
     assert !internal.isShrinking();
diff --git a/src/main/java/com/android/tools/r8/DesugarGraphConsumer.java b/src/main/java/com/android/tools/r8/DesugarGraphConsumer.java
new file mode 100644
index 0000000..f771dd5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/DesugarGraphConsumer.java
@@ -0,0 +1,26 @@
+// 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 com.android.tools.r8.origin.Origin;
+
+/** Consumer for receiving dependency edges for desugaring. */
+@KeepForSubclassing
+public interface DesugarGraphConsumer {
+
+  /**
+   * Callback indicating that code originating from {@code src} was used to correctly desugar some
+   * code originating from {@code dst}.
+   *
+   * <p>In other words, {@code src} is a dependency for the desugaring of {@code dst}.
+   *
+   * <p>Note: this callback may be called on multiple threads.
+   *
+   * <p>Note: this callback places no guarantees on order of calls or on duplicate calls.
+   *
+   * @param src Origin of some code input that is needed to desugar {@code dst}.
+   * @param dst Origin of some code that was dependent on code in {@code src}.
+   */
+  void accept(Origin src, Origin dst);
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java b/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java
index 044b50b..6833067 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java
@@ -186,6 +186,8 @@
       } else {
         DexClass superClass = appView.definitionFor(current.superType);
         if (superClass != null) {
+          // TODO(b/138988172): Can we avoid traversing the full hierarchy for each type?
+          InterfaceMethodRewriter.reportDependencyEdge(superClass, current, appView);
           current = superClass;
         } else {
           String message = "Default method desugaring of `" + clazz.toSourceString() + "` failed";
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
index a241bf9..5f49c84 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.ir.desugar;
 
+import com.android.tools.r8.DesugarGraphConsumer;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.graph.AppInfo;
@@ -1125,7 +1126,6 @@
       warnMissingInterface(classToDesugar, implementing, iface);
       return helper.wrapInCollection();
     }
-
     if (!definedInterface.isInterface()) {
       throw new CompilationError(
           "Type " + iface.toSourceString() + " is referenced as an interface from `"
@@ -1140,6 +1140,12 @@
       return helper.wrapInCollection();
     }
 
+    // At this point we likely have a non-library type that may depend on default method information
+    // from its interfaces and the dependency should be reported.
+    if (!definedInterface.isLibraryClass()) {
+      reportDependencyEdge(definedInterface, implementing, appView);
+    }
+
     // Merge information from all superinterfaces.
     for (DexType superinterface : definedInterface.interfaces.values) {
       helper.merge(getOrCreateInterfaceInfo(classToDesugar, definedInterface, superinterface));
@@ -1159,4 +1165,16 @@
 
     return helper.wrapInCollection();
   }
+
+  public static void reportDependencyEdge(
+      DexClass dependency, DexClass dependent, AppView<?> appView) {
+    DesugarGraphConsumer consumer = appView.options().desugarGraphConsumer;
+    if (consumer != null) {
+      Origin dependencyOrigin = dependency.getOrigin();
+      Origin dependentOrigin = dependent.getOrigin();
+      if (dependencyOrigin != dependentOrigin) {
+        consumer.accept(dependencyOrigin, dependentOrigin);
+      }
+    }
+  }
 }
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 e4b84f6..b9dd6c6 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.DataResourceConsumer;
 import com.android.tools.r8.DataResourceProvider;
+import com.android.tools.r8.DesugarGraphConsumer;
 import com.android.tools.r8.DexFilePerClassFileConsumer;
 import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.ProgramConsumer;
@@ -638,6 +639,10 @@
   // of the resulting program must be reported to the consumer.
   public GraphConsumer mainDexKeptGraphConsumer = null;
 
+  // If null, no desugaring dependencies need to be provided. If non-null, each dependency between
+  // code objects needed for correct desugaring needs to be provided to the consumer.
+  public DesugarGraphConsumer desugarGraphConsumer = null;
+
   public Path proguardCompatibilityRulesOutput = null;
   public Consumer<List<ProguardConfigurationRule>> syntheticProguardRulesConsumer = null;
 
diff --git a/src/test/java/com/android/tools/r8/desugar/graph/InterfaceToImplementingClassDependencyTest.java b/src/test/java/com/android/tools/r8/desugar/graph/InterfaceToImplementingClassDependencyTest.java
new file mode 100644
index 0000000..703a237
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/graph/InterfaceToImplementingClassDependencyTest.java
@@ -0,0 +1,118 @@
+// 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.desugar.graph;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.DesugarGraphConsumer;
+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.origin.Origin;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+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 InterfaceToImplementingClassDependencyTest extends TestBase {
+
+  // The simplest program that gives rise to a dependency edge in the graph is an interface with
+  // an implementing class. In this case, changes to the interface could require re-dexing the
+  // implementing class despite no derived changes happing to the classfile for the implementing
+  // class.
+  //
+  // For example, adding default method I.foo will trigger javac compilation of I and A to I.class
+  // and A.class, here I.class will have changed, but A.class is unchanged, thus only I.class will
+  // be compiled to dex if nothing informs about the desugaring dependency. That is incorrect,
+  // since A.dex needs to contain the desugared code for calling the default method.
+  //
+  // Note: the dependency of I for the compilation of A exists even when no default methods do.
+
+  public interface I {
+    // Emtpy.
+  }
+
+  public class A implements I {
+    // Empty.
+  }
+
+  public static class TestClass {
+
+    public static void main(String[] args) {
+      System.out.println("Hello World!");
+    }
+  }
+
+  // Test runner follows.
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevels().build();
+  }
+
+  private final TestParameters parameters;
+
+  public InterfaceToImplementingClassDependencyTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    if (parameters.isCfRuntime()) {
+      testForJvm()
+          .addProgramClasses(I.class, A.class, TestClass.class)
+          .run(parameters.getRuntime(), TestClass.class)
+          .assertSuccessWithOutputLines("Hello World!");
+    } else {
+      MyDesugarGraphConsumer consumer = new MyDesugarGraphConsumer();
+      Origin originI = makeOrigin("I");
+      Origin originA = makeOrigin("A");
+      testForD8()
+          .setMinApi(parameters.getApiLevel())
+          .addProgramClasses(TestClass.class)
+          .apply(
+              b ->
+                  b.getBuilder()
+                      .setDesugarGraphConsumer(consumer)
+                      .addClassProgramData(ToolHelper.getClassAsBytes(I.class), originI)
+                      .addClassProgramData(ToolHelper.getClassAsBytes(A.class), originA))
+          .run(parameters.getRuntime(), TestClass.class)
+          .assertSuccessWithOutputLines("Hello World!");
+      // If API level indicates desugaring is needed check the edges are reported.
+      if (parameters.getApiLevel().getLevel() < AndroidApiLevel.N.getLevel()) {
+        assertEquals(1, consumer.edges.size());
+        assertEquals(originI, consumer.edges.keySet().iterator().next());
+        assertEquals(originA, consumer.edges.values().iterator().next().iterator().next());
+      } else {
+        assertEquals(0, consumer.edges.size());
+      }
+    }
+  }
+
+  private Origin makeOrigin(String name) {
+    return new Origin(Origin.root()) {
+      @Override
+      public String part() {
+        return name;
+      }
+    };
+  }
+
+  public static class MyDesugarGraphConsumer implements DesugarGraphConsumer {
+
+    Map<Origin, Set<Origin>> edges = new HashMap<>();
+
+    @Override
+    public synchronized void accept(Origin src, Origin dst) {
+      edges.computeIfAbsent(src, s -> new HashSet<>()).add(dst);
+    }
+  }
+}