Add support for -checkdiscard and -whyareyoukeeping for main dex rules

Bug: 116774422
Change-Id: Ic97a0303c202e414a32f54e7c28f2747faf749a3
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 6436654..8be6306 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -424,14 +424,23 @@
         AppInfoWithLiveness mainDexAppInfo =
             enqueuer.traceMainDex(mainDexRootSet, executorService, timing);
 
-        // LiveTypes is the result.
+        // LiveTypes is the tracing result.
         Set<DexType> mainDexBaseClasses = new HashSet<>(mainDexAppInfo.liveTypes);
-
         // Calculate the automatic main dex list according to legacy multidex constraints.
-        // Add those classes to an eventual manual list of classes.
-        application = application.builder()
-            .addToMainDexList(new MainDexListBuilder(mainDexBaseClasses, application).run())
-            .build();
+        Set<DexType> mainDexClasses = new MainDexListBuilder(mainDexBaseClasses, application).run();
+        if (!mainDexRootSet.checkDiscarded.isEmpty()) {
+          new DiscardedChecker(mainDexRootSet, mainDexClasses, appView.appInfo(), options).run();
+        }
+        if (!mainDexRootSet.reasonAsked.isEmpty()) {
+          // If the main dex rules have -whyareyoukeeping rules build an application
+          // pruned with the main dex tracing result to reflect only on main dex content.
+          TreePruner mainDexPruner = new TreePruner(application, mainDexAppInfo, options);
+          DexApplication mainDexApplication = mainDexPruner.run();
+          ReasonPrinter reasonPrinter = enqueuer.getReasonPrinter(mainDexRootSet.reasonAsked);
+          reasonPrinter.run(mainDexApplication);
+        }
+        // Add automatic main dex classes to an eventual manual list of classes.
+        application = application.builder().addToMainDexList(mainDexClasses).build();
       }
 
       appView.setAppInfo(new AppInfoWithSubtyping(application));
diff --git a/src/main/java/com/android/tools/r8/shaking/DiscardedChecker.java b/src/main/java/com/android/tools/r8/shaking/DiscardedChecker.java
index e3348a6..b28a1a7 100644
--- a/src/main/java/com/android/tools/r8/shaking/DiscardedChecker.java
+++ b/src/main/java/com/android/tools/r8/shaking/DiscardedChecker.java
@@ -4,29 +4,47 @@
 package com.android.tools.r8.shaking;
 
 import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexDefinition;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.StringDiagnostic;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Set;
 
 public class DiscardedChecker {
 
   private final Set<DexDefinition> checkDiscarded;
-  private final DexApplication application;
+  private final List<DexProgramClass> classes;
   private boolean fail = false;
   private final InternalOptions options;
 
   public DiscardedChecker(RootSet rootSet, DexApplication application, InternalOptions options) {
     this.checkDiscarded = rootSet.checkDiscarded;
-    this.application = application;
+    this.classes = application.classes();
+    this.options = options;
+  }
+
+  public DiscardedChecker(
+      RootSet rootSet, Set<DexType> types, AppInfo appInfo, InternalOptions options) {
+    this.checkDiscarded = rootSet.checkDiscarded;
+    this.classes = new ArrayList<>();
+    types.forEach(
+        type -> {
+          DexClass clazz = appInfo.definitionFor(type);
+          assert clazz.isProgramClass();
+          this.classes.add(clazz.asProgramClass());
+        });
     this.options = options;
   }
 
   public void run() {
-    for (DexProgramClass clazz : application.classes()) {
+    for (DexProgramClass clazz : classes) {
       checkItem(clazz);
       clazz.forEachMethod(this::checkItem);
       clazz.forEachField(this::checkItem);
diff --git a/src/test/java/com/android/tools/r8/maindexlist/checkdiscard/MainDexListCheckDiscard.java b/src/test/java/com/android/tools/r8/maindexlist/checkdiscard/MainDexListCheckDiscard.java
new file mode 100644
index 0000000..a68ae3b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/maindexlist/checkdiscard/MainDexListCheckDiscard.java
@@ -0,0 +1,58 @@
+// Copyright (c) 2018, 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.maindexlist.checkdiscard;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.origin.Origin;
+import com.google.common.collect.ImmutableList;
+import org.junit.Assert;
+import org.junit.Test;
+
+class HelloWorldMain {
+  public static void main(String[] args) {
+    System.out.println(new MainDexClass());
+  }
+}
+
+class MainDexClass {}
+
+class NonMainDexClass {}
+
+public class MainDexListCheckDiscard extends TestBase {
+  public void runTest(String checkDiscardRule, boolean shouldFail) throws Exception {
+    R8Command command =
+        ToolHelper.prepareR8CommandBuilder(
+                readClasses(HelloWorldMain.class, MainDexClass.class, NonMainDexClass.class))
+            .addMainDexRules(
+                ImmutableList.of(keepMainProguardConfiguration(HelloWorldMain.class)),
+                Origin.unknown())
+            .addMainDexRules(ImmutableList.of(checkDiscardRule), Origin.unknown())
+            .setOutput(temp.getRoot().toPath(), OutputMode.DexIndexed)
+            .setMode(CompilationMode.RELEASE)
+            .build();
+    try {
+      ToolHelper.runR8(command);
+    } catch (CompilationFailedException e) {
+      Assert.assertTrue(shouldFail);
+      return;
+    }
+    Assert.assertFalse(shouldFail);
+  }
+
+  @Test
+  public void testMainDexClassNotDiscarded() throws Exception {
+    runTest("-checkdiscard class " + MainDexClass.class.getCanonicalName(), true);
+  }
+
+  @Test
+  public void testNonMainDexClassDiscarded() throws Exception {
+    runTest("-checkdiscard class " + NonMainDexClass.class.getCanonicalName(), false);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/maindexlist/whyareyoukeeping/MainDexListWhyAreYouKeeping.java b/src/test/java/com/android/tools/r8/maindexlist/whyareyoukeeping/MainDexListWhyAreYouKeeping.java
new file mode 100644
index 0000000..2713c6d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/maindexlist/whyareyoukeeping/MainDexListWhyAreYouKeeping.java
@@ -0,0 +1,67 @@
+// Copyright (c) 2018, 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.maindexlist.whyareyoukeeping;
+
+import static org.hamcrest.core.StringContains.containsString;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.origin.Origin;
+import com.google.common.collect.ImmutableList;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.nio.charset.Charset;
+import org.junit.Test;
+
+class HelloWorldMain {
+  public static void main(String[] args) {
+    System.out.println(new MainDexClass());
+  }
+}
+
+class MainDexClass {}
+
+class NonMainDexClass {}
+
+public class MainDexListWhyAreYouKeeping extends TestBase {
+  public String runTest(String whyAreYouKeepingRule) throws Exception {
+    R8Command command =
+        ToolHelper.prepareR8CommandBuilder(
+                readClasses(HelloWorldMain.class, MainDexClass.class, NonMainDexClass.class))
+            .addMainDexRules(
+                ImmutableList.of(keepMainProguardConfiguration(HelloWorldMain.class)),
+                Origin.unknown())
+            .addMainDexRules(ImmutableList.of(whyAreYouKeepingRule), Origin.unknown())
+            .setOutput(temp.getRoot().toPath(), OutputMode.DexIndexed)
+            .setMode(CompilationMode.RELEASE)
+            .build();
+    PrintStream stdout = System.out;
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    System.setOut(new PrintStream(baos));
+    ToolHelper.runR8(command);
+    String output = new String(baos.toByteArray(), Charset.defaultCharset());
+    System.setOut(stdout);
+    return output;
+  }
+
+  @Test
+  public void testMainDexClassWhyAreYouKeeping() throws Exception {
+    String output = runTest("-whyareyoukeeping class " + MainDexClass.class.getCanonicalName());
+    assertThat(
+        output, containsString("com.android.tools.r8.maindexlist.whyareyoukeeping.MainDexClass"));
+    assertThat(output, containsString("- is live because referenced in keep rule:"));
+  }
+
+  @Test
+  public void testNonMainDexWhyAreYouKeeping() throws Exception {
+    String output = runTest("-whyareyoukeeping class " + NonMainDexClass.class.getCanonicalName());
+    assertTrue(output.isEmpty());
+  }
+}