Introduce ApiLevelException as a checked exception

It extends CompilationException which is already part of the API.

Bug: 63692875
Change-Id: I79d37675f04c0707383c7315b4e1a423f440579e
diff --git a/src/main/java/com/android/tools/r8/ApiLevelException.java b/src/main/java/com/android/tools/r8/ApiLevelException.java
new file mode 100644
index 0000000..0087086
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ApiLevelException.java
@@ -0,0 +1,40 @@
+// Copyright (c) 2017, 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;
+
+/**
+ * Exception to signal features that are not supported until a given API level.
+ */
+public class ApiLevelException extends CompilationException {
+
+  private final int minApiLevel;
+  private final String minApiLevelString;
+  private final String unsupportedFeatures;
+  private final String sourceString;
+
+  public ApiLevelException(
+      int minApiLevel, String minApiLevelString, String unsupportedFeatures, String sourceString) {
+    super("");
+    assert minApiLevel > 0;
+    assert minApiLevelString != null;
+    assert unsupportedFeatures != null;
+    this.minApiLevel = minApiLevel;
+    this.minApiLevelString = minApiLevelString;
+    this.unsupportedFeatures = unsupportedFeatures;
+    this.sourceString = sourceString;
+  }
+
+  @Override
+  public String getMessage() {
+    String message =
+        unsupportedFeatures
+            + " are only supported starting with "
+            + minApiLevelString
+            + " (--min-api "
+            + minApiLevel
+            + ")";
+    message = (sourceString != null) ? message + ": " + sourceString : message;
+    return message;
+  }
+}
\ No newline at end of file
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 6988b06..cbbec9e 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -62,7 +62,7 @@
    * @param command D8 command.
    * @return the compilation result.
    */
-  public static D8Output run(D8Command command) throws IOException {
+  public static D8Output run(D8Command command) throws IOException, CompilationException {
     InternalOptions options = command.getInternalOptions();
     CompilationResult result = runForTesting(command.getInputApp(), options);
     assert result != null;
@@ -83,7 +83,8 @@
    * @param executor executor service from which to get threads for multi-threaded processing.
    * @return the compilation result.
    */
-  public static D8Output run(D8Command command, ExecutorService executor) throws IOException {
+  public static D8Output run(D8Command command, ExecutorService executor)
+      throws IOException, CompilationException {
     InternalOptions options = command.getInternalOptions();
     CompilationResult result = runForTesting(
         command.getInputApp(), options, executor);
@@ -142,7 +143,7 @@
   }
 
   static CompilationResult runForTesting(AndroidApp inputApp, InternalOptions options)
-      throws IOException {
+      throws IOException, CompilationException {
     ExecutorService executor = ThreadUtils.getExecutorService(options);
     try {
       return runForTesting(inputApp, options, executor);
@@ -152,7 +153,8 @@
   }
 
   static CompilationResult runForTesting(
-      AndroidApp inputApp, InternalOptions options, ExecutorService executor) throws IOException {
+      AndroidApp inputApp, InternalOptions options, ExecutorService executor)
+      throws IOException, CompilationException {
     try {
       assert !inputApp.hasPackageDistribution();
 
@@ -187,6 +189,8 @@
     } catch (ExecutionException e) {
       if (e.getCause() instanceof CompilationError) {
         throw (CompilationError) e.getCause();
+      } else if (e.getCause() instanceof CompilationException) {
+        throw (CompilationException) e.getCause();
       } else {
         throw new RuntimeException(e.getMessage(), e.getCause());
       }
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
index 6586a57..76d434c 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.dex;
 
+import com.android.tools.r8.ApiLevelException;
 import com.android.tools.r8.dex.VirtualFile.FilePerClassDistributor;
 import com.android.tools.r8.dex.VirtualFile.FillFilesDistributor;
 import com.android.tools.r8.dex.VirtualFile.PackageMapDistributor;
@@ -196,7 +197,7 @@
     }
   }
 
-  private byte[] writeDexFile(VirtualFile vfile) {
+  private byte[] writeDexFile(VirtualFile vfile) throws ApiLevelException {
     FileWriter fileWriter =
         new FileWriter(
             vfile.computeMapping(application), application, appInfo, options, namingLens);
diff --git a/src/main/java/com/android/tools/r8/dex/FileWriter.java b/src/main/java/com/android/tools/r8/dex/FileWriter.java
index 332f292..90a5dcf 100644
--- a/src/main/java/com/android/tools/r8/dex/FileWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/FileWriter.java
@@ -7,6 +7,7 @@
 
 import com.google.common.collect.Sets;
 
+import com.android.tools.r8.ApiLevelException;
 import com.android.tools.r8.code.Instruction;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.graph.AppInfo;
@@ -186,7 +187,7 @@
     return this;
   }
 
-  public byte[] generate() {
+  public byte[] generate() throws ApiLevelException {
     // Check restrictions on interface methods.
     checkInterfaceMethods();
 
@@ -271,10 +272,15 @@
     Arrays.sort(methods, (DexEncodedMethod a, DexEncodedMethod b) -> a.method.compareTo(b.method));
   }
 
-  private void checkInterfaceMethods() {
+  private void checkInterfaceMethods() throws ApiLevelException {
     for (DexProgramClass clazz : mapping.getClasses()) {
       if (clazz.isInterface()) {
-        clazz.forEachMethod(this::checkInterfaceMethod);
+        for (DexEncodedMethod method : clazz.directMethods()) {
+          checkInterfaceMethod(method);
+        }
+        for (DexEncodedMethod method : clazz.virtualMethods()) {
+          checkInterfaceMethod(method);
+        }
       }
     }
   }
@@ -285,15 +291,18 @@
   //  -- starting with N interfaces may also have public or private
   //     static methods, as well as public non-abstract (default)
   //     and private instance methods.
-  private void checkInterfaceMethod(DexEncodedMethod method) {
+  private void checkInterfaceMethod(DexEncodedMethod method)
+      throws ApiLevelException {
     if (application.dexItemFactory.isClassConstructor(method.method)) {
       return; // Class constructor is always OK.
     }
     if (method.accessFlags.isStatic()) {
       if (!options.canUseDefaultAndStaticInterfaceMethods()) {
-        throw new CompilationError("Static interface methods are only supported "
-            + "starting with Android N (--min-api " + Constants.ANDROID_N_API + "): "
-            + method.method.toSourceString());
+        throw new ApiLevelException(
+            Constants.ANDROID_N_API,
+            "Android N",
+            "Static interface methods",
+            method.method.toSourceString());
       }
 
     } else {
@@ -303,9 +312,11 @@
       }
       if (!method.accessFlags.isAbstract() && !method.accessFlags.isPrivate() &&
           !options.canUseDefaultAndStaticInterfaceMethods()) {
-        throw new CompilationError("Default interface methods are only supported "
-            + "starting with Android N (--min-api " + Constants.ANDROID_N_API + "): "
-            + method.method.toSourceString());
+        throw new ApiLevelException(
+            Constants.ANDROID_N_API,
+            "Android N",
+            "Default interface methods",
+            method.method.toSourceString());
       }
     }
 
@@ -313,9 +324,11 @@
       if (options.canUsePrivateInterfaceMethods()) {
         return;
       }
-      throw new CompilationError("Private interface methods are only supported "
-          + "starting with Android N (--min-api " + Constants.ANDROID_N_API + "): "
-          + method.method.toSourceString());
+      throw new ApiLevelException(
+          Constants.ANDROID_N_API,
+          "Android N",
+          "Private interface methods",
+          method.method.toSourceString());
     }
 
     if (!method.accessFlags.isPublic()) {
@@ -356,13 +369,21 @@
   }
 
   private <T extends DexItem> void writeFixedSectionItems(T[] items, int offset,
-      Consumer<T> writer) {
+      ItemWriter<T> writer) throws ApiLevelException {
     assert dest.position() == offset;
     for (T item : items) {
       writer.accept(item);
     }
   }
 
+  /**
+   * Similar to a {@link Consumer} but throws an {@link ApiLevelException}.
+   */
+  @FunctionalInterface
+  private interface ItemWriter<T> {
+    void accept(T t) throws ApiLevelException;
+  }
+
   private <T extends DexItem> void writeItems(Collection<T> items, Consumer<Integer> offsetSetter,
       Consumer<T> writer) {
     writeItems(items, offsetSetter, writer, 1);
@@ -663,7 +684,7 @@
     }
   }
 
-  private void writeMethodHandle(DexMethodHandle methodHandle) {
+  private void writeMethodHandle(DexMethodHandle methodHandle) throws ApiLevelException {
     checkThatInvokeCustomIsAllowed();
     MethodHandleType methodHandleDexType;
     switch (methodHandle.type) {
@@ -692,7 +713,7 @@
     dest.putShort((short) 0); // unused
   }
 
-  private void writeCallSite(DexCallSite callSite) {
+  private void writeCallSite(DexCallSite callSite) throws ApiLevelException {
     checkThatInvokeCustomIsAllowed();
     assert dest.isAligned(4);
     dest.putInt(mixedSectionOffsets.getOffsetFor(callSite.getEncodedArray()));
@@ -1342,10 +1363,13 @@
     }
   }
 
-  private void checkThatInvokeCustomIsAllowed() {
+  private void checkThatInvokeCustomIsAllowed() throws ApiLevelException {
     if (!options.canUseInvokeCustom()) {
-      throw new CompilationError("Invoke-custom is unsupported before Android O (--min-api "
-          + Constants.ANDROID_O_API + ")");
+      throw new ApiLevelException(
+          Constants.ANDROID_O_API,
+          "Android O",
+          "Invoke-customs",
+          null /* sourceString */);
     }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index d0032cd..1e5c073 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -505,12 +505,12 @@
     return runD8(D8Command.builder(app).build(), optionsConsumer);
   }
 
-  public static AndroidApp runD8(D8Command command) throws IOException {
+  public static AndroidApp runD8(D8Command command) throws IOException, CompilationException {
     return runD8(command, null);
   }
 
   public static AndroidApp runD8(D8Command command, Consumer<InternalOptions> optionsConsumer)
-      throws IOException {
+      throws IOException, CompilationException {
     InternalOptions options = command.getInternalOptions();
     if (optionsConsumer != null) {
       optionsConsumer.accept(options);
diff --git a/src/test/java/com/android/tools/r8/internal/CompilationTestBase.java b/src/test/java/com/android/tools/r8/internal/CompilationTestBase.java
index 4b2ee49..46f2fd3 100644
--- a/src/test/java/com/android/tools/r8/internal/CompilationTestBase.java
+++ b/src/test/java/com/android/tools/r8/internal/CompilationTestBase.java
@@ -49,7 +49,7 @@
   }
 
   public AndroidApp runAndCheckVerification(D8Command command, String referenceApk)
-      throws IOException, ExecutionException {
+      throws IOException, ExecutionException, CompilationException {
     return checkVerification(ToolHelper.runD8(command), referenceApk);
   }