Better multi-dex error message.

It informs the user of:
  1) whether s/he can try using multi-dex
      or it exceeds the main-dex list limit; and
  2) what exceeds the limit: num of either methods or fields.

Bug: 63170711
Change-Id: Id6ceccbdeae3005b0c8da7172925712f25b12d43
diff --git a/src/main/java/com/android/tools/r8/dex/DexFile.java b/src/main/java/com/android/tools/r8/dex/DexFile.java
index a2c65d3..3441346 100644
--- a/src/main/java/com/android/tools/r8/dex/DexFile.java
+++ b/src/main/java/com/android/tools/r8/dex/DexFile.java
@@ -62,13 +62,13 @@
     int version;
     switch (versionByte) {
       case '8':
-        version = 38;
+        version = Constants.ANDROID_O_DEX_VERSION;
         break;
       case '7':
-        version = 37;
+        version = Constants.ANDROID_N_DEX_VERSION;
         break;
       case '5':
-        version = 35;
+        version = Constants.ANDROID_PRE_N_DEX_VERSION;
         break;
       default:
         throw new CompilationError("Dex file has invalid version number: " + name);
diff --git a/src/main/java/com/android/tools/r8/dex/VirtualFile.java b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
index f336a3d..f309670 100644
--- a/src/main/java/com/android/tools/r8/dex/VirtualFile.java
+++ b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
@@ -174,6 +174,35 @@
     return isFull(transaction.getNumberOfMethods(), transaction.getNumberOfFields(), MAX_ENTRIES);
   }
 
+  void throwIfFull(boolean multiDexEnabled) {
+    if (!isFull()) {
+      return;
+    }
+    StringBuilder messageBuilder = new StringBuilder();
+    // General message: Cannot fit.
+    messageBuilder.append("Cannot fit requested classes in ");
+    messageBuilder.append(multiDexEnabled ? "the main-" : "a single ");
+    messageBuilder.append("dex file.\n");
+    // Suggest supplying the main-dex list or explicitly mention that main-dex list is too large.
+    if (multiDexEnabled) {
+      messageBuilder.append("The list of classes for the main-dex list is too large.\n");
+    } else {
+      messageBuilder.append("Try supplying a main-dex list.\n");
+    }
+    // Show the numbers of methods and/or fields that exceed the limit.
+    if (transaction.getNumberOfMethods() > MAX_ENTRIES) {
+      messageBuilder.append("# methods: ");
+      messageBuilder.append(transaction.getNumberOfMethods());
+      messageBuilder.append(" > ").append(MAX_ENTRIES).append('\n');
+    }
+    if (transaction.getNumberOfFields() > MAX_ENTRIES) {
+      messageBuilder.append("# fields: ");
+      messageBuilder.append(transaction.getNumberOfFields());
+      messageBuilder.append(" > ").append(MAX_ENTRIES).append('\n');
+    }
+    throw new CompilationError(messageBuilder.toString());
+  }
+
   private boolean isFilledEnough(FillStrategy fillStrategy) {
     return isFull(
         transaction.getNumberOfMethods(),
@@ -202,7 +231,7 @@
     protected final ApplicationWriter writer;
     protected final Map<Integer, VirtualFile> nameToFileMap = new HashMap<>();
 
-    public Distributor(ApplicationWriter writer) {
+    Distributor(ApplicationWriter writer) {
       this.application = writer.application;
       this.writer = writer;
     }
@@ -212,7 +241,7 @@
 
   public static class FilePerClassDistributor extends Distributor {
 
-    public FilePerClassDistributor(ApplicationWriter writer) {
+    FilePerClassDistributor(ApplicationWriter writer) {
       super(writer);
     }
 
@@ -232,7 +261,7 @@
     protected Set<DexProgramClass> classes;
     protected Map<DexProgramClass, String> originalNames;
 
-    public DistributorBase(ApplicationWriter writer) {
+    DistributorBase(ApplicationWriter writer) {
       super(writer);
 
       classes = Sets.newHashSet(application.classes());
@@ -247,9 +276,7 @@
           if (clazz != null && clazz.isProgramClass()) {
             DexProgramClass programClass = (DexProgramClass) clazz;
             mainDexFile.addClass(programClass);
-            if (mainDexFile.isFull()) {
-              throw new CompilationError("Cannot fit requested classes in main-dex file.");
-            }
+            mainDexFile.throwIfFull(true);
             classes.remove(programClass);
           } else {
             System.out.println(
@@ -298,7 +325,7 @@
   public static class FillFilesDistributor extends DistributorBase {
     private final FillStrategy fillStrategy;
 
-    public FillFilesDistributor(ApplicationWriter writer, boolean minimalMainDex) {
+    FillFilesDistributor(ApplicationWriter writer, boolean minimalMainDex) {
       super(writer);
       this.fillStrategy = minimalMainDex ? FillStrategy.MINIMAL_MAIN_DEX : FillStrategy.FILL_MAX;
     }
@@ -326,7 +353,7 @@
   }
 
   public static class MonoDexDistributor extends DistributorBase {
-    public MonoDexDistributor(ApplicationWriter writer) {
+    MonoDexDistributor(ApplicationWriter writer) {
       super(writer);
     }
 
@@ -337,9 +364,7 @@
 
       for (DexProgramClass programClass : classes) {
         mainDexFile.addClass(programClass);
-        if (mainDexFile.isFull()) {
-          throw new CompilationError("Cannot fit all classes in a single dex file.");
-        }
+        mainDexFile.throwIfFull(false);
       }
       mainDexFile.commitTransaction();
       return nameToFileMap;
@@ -350,7 +375,7 @@
     private final PackageDistribution packageDistribution;
     private final ExecutorService executorService;
 
-    public PackageMapDistributor(
+    PackageMapDistributor(
         ApplicationWriter writer,
         PackageDistribution packageDistribution,
         ExecutorService executorService) {
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
index 69fdd65..a5e2309 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
@@ -150,8 +150,15 @@
 
   @Test
   public void cannotFitBothIntoMainDex() throws Throwable {
-    thrown.expect(CompilationError.class);
-    verifyMainDexContains(TWO_LARGE_CLASSES, getTwoLargeClassesAppPath(), false);
+    try {
+      verifyMainDexContains(TWO_LARGE_CLASSES, getTwoLargeClassesAppPath(), false);
+      fail("Expect to fail, for there are too many classes for the main-dex list.");
+    } catch (CompilationError e) {
+      // Make sure {@link MonoDexDistributor} was _not_ used.
+      assertFalse(e.getMessage().contains("single dex file"));
+      // Make sure what exceeds the limit is the number of methods.
+      assertTrue(e.getMessage().contains("# methods"));
+    }
   }
 
   @Test
@@ -183,8 +190,15 @@
 
   @Test
   public void cannotFitAllIntoMainDex() throws Throwable {
-    thrown.expect(CompilationError.class);
-    verifyMainDexContains(MANY_CLASSES, getManyClassesMultiDexAppPath(), false);
+    try {
+      verifyMainDexContains(MANY_CLASSES, getManyClassesMultiDexAppPath(), false);
+      fail("Expect to fail, for there are too many classes for the main-dex list.");
+    } catch (CompilationError e) {
+      // Make sure {@link MonoDexDistributor} was _not_ used.
+      assertFalse(e.getMessage().contains("single dex file"));
+      // Make sure what exceeds the limit is the number of methods.
+      assertTrue(e.getMessage().contains("# methods"));
+    }
   }
 
   @Test
@@ -334,7 +348,10 @@
           MANY_CLASSES, Constants.ANDROID_K_API, false, MANY_CLASSES_MULTI_DEX_METHODS_PER_CLASS);
       fail("Expect to fail, for there are many classes while multidex is not enabled.");
     } catch (CompilationError e) {
-      assertTrue(e.getMessage().contains("Cannot fit all classes in a single dex file."));
+      // Make sure {@link MonoDexDistributor} was used.
+      assertTrue(e.getMessage().contains("single dex file"));
+      // Make sure what exceeds the limit is the number of methods.
+      assertTrue(e.getMessage().contains("# methods"));
     }
   }
 
@@ -476,8 +493,8 @@
     }
     DexApplication application = builder.build();
     AppInfoWithSubtyping appInfo = new AppInfoWithSubtyping(application);
-    ApplicationWriter writer =
-        new ApplicationWriter(application, appInfo, options, null, NamingLens.getIdentityLens(), null);
+    ApplicationWriter writer = new ApplicationWriter(
+        application, appInfo, options, null, NamingLens.getIdentityLens(), null);
     ExecutorService executor = ThreadUtils.getExecutorService(options);
     try {
       return writer.write(null, executor);