Proposed minimal DiagnosticsHandler API.

Add a method to D8Command and R8Command to supply a
Diagnostics handler that will be used for all diagnostic information
from the compilers.

This is currently only used for CompilationException and the warnings
generated in connection with default interface method desugaring.
Once we agree on the interface, we should make sure that we use this
instead of stdout and stderr for all warning and info diagnostic.

The current default implementation of the API keeps the behavior of
the current code. However, the API allows us to extend the Diagnostic
messages with more information over time. Also, with errors passed
to the DiagnosticsHandler we can consider making certain errors
non-fatal and keep on trucking to get more errors reported in one
compilation.

R=benoitlamarche@google.com, gavra@google.com, zerny@google.com

Change-Id: Iedd0b7d71460c14a78c13c09b340e491071c2fae
Bug: 67087703
diff --git a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
index e659231..72cfbab 100644
--- a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
+++ b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
@@ -3,9 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
-import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DefaultDiagnosticsHandler;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.OutputMode;
 import java.nio.file.Path;
@@ -21,14 +21,15 @@
   private final OutputMode outputMode;
   private final CompilationMode mode;
   private final int minApiLevel;
+  private final DiagnosticsHandler diagnosticsHandler;
 
   BaseCompilerCommand(boolean printHelp, boolean printVersion) {
     super(printHelp, printVersion);
-
-    this.outputPath = null;
-    this.outputMode = OutputMode.Indexed;
-    this.mode = null;
-    this.minApiLevel = 0;
+    outputPath = null;
+    outputMode = OutputMode.Indexed;
+    mode = null;
+    minApiLevel = 0;
+    diagnosticsHandler = new DefaultDiagnosticsHandler();
   }
 
   BaseCompilerCommand(
@@ -36,7 +37,8 @@
       Path outputPath,
       OutputMode outputMode,
       CompilationMode mode,
-      int minApiLevel) {
+      int minApiLevel,
+      DiagnosticsHandler diagnosticsHandler) {
     super(app);
     assert mode != null;
     assert minApiLevel > 0;
@@ -44,6 +46,7 @@
     this.outputMode = outputMode;
     this.mode = mode;
     this.minApiLevel = minApiLevel;
+    this.diagnosticsHandler = diagnosticsHandler;
   }
 
   public Path getOutputPath() {
@@ -62,6 +65,10 @@
     return outputMode;
   }
 
+  public DiagnosticsHandler getDiagnosticsHandler() {
+    return diagnosticsHandler;
+  }
+
   abstract public static class Builder<C extends BaseCompilerCommand, B extends Builder<C, B>>
       extends BaseCommand.Builder<C, B> {
 
@@ -69,6 +76,7 @@
     private OutputMode outputMode = OutputMode.Indexed;
     private CompilationMode mode;
     private int minApiLevel = AndroidApiLevel.getDefault().getLevel();
+    private DiagnosticsHandler diagnosticsHandler = new DefaultDiagnosticsHandler();
 
     protected Builder(CompilationMode mode) {
       this(AndroidApp.builder(), mode, false);
@@ -135,6 +143,15 @@
       return self();
     }
 
+    public DiagnosticsHandler getDiagnosticsHandler() {
+      return diagnosticsHandler;
+    }
+
+    public B setDiagnosticsHandler(DiagnosticsHandler diagnosticsHandler) {
+      this.diagnosticsHandler = diagnosticsHandler;
+      return self();
+    }
+
     protected void validate() throws CompilationException {
       super.validate();
       if (getAppBuilder().hasMainDexList() && outputMode == OutputMode.FilePerInputClass) {
diff --git a/src/main/java/com/android/tools/r8/CompilationException.java b/src/main/java/com/android/tools/r8/CompilationException.java
index 8e1e56c..c7477b6 100644
--- a/src/main/java/com/android/tools/r8/CompilationException.java
+++ b/src/main/java/com/android/tools/r8/CompilationException.java
@@ -9,7 +9,7 @@
  * This is always an expected error and considered a user input issue.
  * A user-understandable message must be provided.
  */
-public class CompilationException extends Exception {
+public class CompilationException extends Exception implements Diagnostic {
   private static final long serialVersionUID = 1L;
 
   /**
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 9b79616..19f478a 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -60,18 +60,26 @@
   /**
    * Main API entry for the D8 dexer.
    *
+   * <p>If the D8Command contains a DiagnosticsHandler that does not throw a CompilationException
+   * on error this method returns null if the run fails.
+   *
    * @param command D8 command.
    * @return the compilation result.
    */
   public static D8Output run(D8Command command) throws IOException, CompilationException {
     InternalOptions options = command.getInternalOptions();
-    CompilationResult result = runForTesting(command.getInputApp(), options);
-    assert result != null;
-    D8Output output = new D8Output(result.androidApp, command.getOutputMode());
-    if (command.getOutputPath() != null) {
-      output.write(command.getOutputPath());
+    try {
+      CompilationResult result = runForTesting(command.getInputApp(), options);
+      assert result != null;
+      D8Output output = new D8Output(result.androidApp, command.getOutputMode());
+      if (command.getOutputPath() != null) {
+        output.write(command.getOutputPath());
+      }
+      return output;
+    } catch (CompilationException e) {
+      options.diagnosticsHandler.error(e);
+      return null;
     }
-    return output;
   }
 
   /**
@@ -80,21 +88,29 @@
    * <p>The D8 dexer API is intentionally limited and should "do the right thing" given a set of
    * inputs. If the API does not suffice please contact the R8 team.
    *
+   * <p>If the D8Command contains a DiagnosticsHandler that does not throw a CompilationException
+   * on error this method returns null if the run fails.
+   *
    * @param command D8 command.
    * @param executor executor service from which to get threads for multi-threaded processing.
-   * @return the compilation result.
+   * @return the compilation result
    */
   public static D8Output run(D8Command command, ExecutorService executor)
       throws IOException, CompilationException {
     InternalOptions options = command.getInternalOptions();
-    CompilationResult result = runForTesting(
-        command.getInputApp(), options, executor);
-    assert result != null;
-    D8Output output = new D8Output(result.androidApp, command.getOutputMode());
-    if (command.getOutputPath() != null) {
-      output.write(command.getOutputPath());
+    try {
+      CompilationResult result = runForTesting(
+          command.getInputApp(), options, executor);
+      assert result != null;
+      D8Output output = new D8Output(result.androidApp, command.getOutputMode());
+      if (command.getOutputPath() != null) {
+        output.write(command.getOutputPath());
+      }
+      return output;
+    } catch (CompilationException e) {
+      options.diagnosticsHandler.error(e);
+      return null;
     }
-    return output;
   }
 
   private static void run(String[] args) throws IOException, CompilationException {
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index f2aebc7..ac86781 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -7,7 +7,6 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.OffOrAuto;
 import com.android.tools.r8.utils.OutputMode;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
@@ -99,6 +98,7 @@
           getOutputMode(),
           getMode(),
           getMinApiLevel(),
+          getDiagnosticsHandler(),
           intermediate);
     }
   }
@@ -195,8 +195,9 @@
       OutputMode outputMode,
       CompilationMode mode,
       int minApiLevel,
+      DiagnosticsHandler diagnosticsHandler,
       boolean intermediate) {
-    super(inputApp, outputPath, outputMode, mode, minApiLevel);
+    super(inputApp, outputPath, outputMode, mode, minApiLevel, diagnosticsHandler);
     this.intermediate = intermediate;
   }
 
@@ -224,6 +225,7 @@
     assert internal.outline.enabled;
     internal.outline.enabled = false;
     internal.outputMode = getOutputMode();
+    internal.diagnosticsHandler = getDiagnosticsHandler();
     return internal;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/Diagnostic.java b/src/main/java/com/android/tools/r8/Diagnostic.java
new file mode 100644
index 0000000..6be2748
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/Diagnostic.java
@@ -0,0 +1,11 @@
+// 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;
+
+/**
+ * Interface for all diagnostic message produced by D8 and R8.
+ */
+public interface Diagnostic {
+  String toString();
+}
diff --git a/src/main/java/com/android/tools/r8/DiagnosticsHandler.java b/src/main/java/com/android/tools/r8/DiagnosticsHandler.java
new file mode 100644
index 0000000..4a89c6f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/DiagnosticsHandler.java
@@ -0,0 +1,42 @@
+// 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;
+
+/**
+ * A DiagnosticsHandler can be provided to customize handling of diagnostics information.
+ *
+ * <p>During compilation the error, warning and info methods will be called.
+ */
+public interface DiagnosticsHandler {
+
+  /**
+   * Handle error diagnostics.
+   *
+   * <p>By default this throws the exception.
+   *
+   * @param error CompilationException containing error information.
+   * @throws CompilationException
+   */
+  default void error(CompilationException error) throws CompilationException {
+    throw error;
+  }
+
+  /**
+   * Handle warning diagnostics.
+   *
+   * @param warning Diagnostic containing warning information.
+   */
+  default void warning(Diagnostic warning) {
+    System.err.println(warning.toString());
+  }
+
+  /**
+   * Handle info diagnostics.
+   *
+   * @param info Diagnostic containing the information.
+   */
+  default void info(Diagnostic info) {
+    System.out.println(info.toString());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 45af2b3..899a194 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -57,7 +57,6 @@
 import java.io.PrintStream;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.FileAlreadyExistsException;
-import java.nio.file.Files;
 import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -396,12 +395,15 @@
    * <p>The R8 API is intentionally limited and should "do the right thing" given a command. If this
    * API does not suffice please contact the R8 team.
    *
+   * <p>If the R8Command contains a DiagnosticsHandler that does not throw a CompilationException
+   * on error this method returns null if the run fails.
+   *
    * @param command R8 command.
    * @return the compilation result.
    */
-  public static AndroidApp run(R8Command command)
-      throws IOException, CompilationException {
-    ExecutorService executorService = ThreadUtils.getExecutorService(command.getInternalOptions());
+  public static AndroidApp run(R8Command command) throws IOException, CompilationException {
+    InternalOptions options = command.getInternalOptions();
+    ExecutorService executorService = ThreadUtils.getExecutorService(options);
     try {
       return run(command, executorService);
     } finally {
@@ -476,6 +478,9 @@
    * <p>The R8 API is intentionally limited and should "do the right thing" given a command. If this
    * API does not suffice please contact the R8 team.
    *
+   * <p>If the R8Command contains a DiagnosticsHandler that does not throw a CompilationException
+   * on error this method returns null if the run fails.
+   *
    * @param command R8 command.
    * @param executor executor service from which to get threads for multi-threaded processing.
    * @return the compilation result.
@@ -483,10 +488,15 @@
   public static AndroidApp run(R8Command command, ExecutorService executor)
       throws IOException, CompilationException {
     InternalOptions options = command.getInternalOptions();
-    AndroidApp outputApp =
-        runForTesting(command.getInputApp(), options, executor).androidApp;
-    writeOutputs(command, options, outputApp);
-    return outputApp;
+    try {
+      AndroidApp outputApp =
+          runForTesting(command.getInputApp(), options, executor).androidApp;
+      writeOutputs(command, options, outputApp);
+      return outputApp;
+    } catch (CompilationException e) {
+      options.diagnosticsHandler.error(e);
+      return null;
+    }
   }
 
   private static void run(String[] args)
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 5e0b7d4..a6a1c33 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -237,6 +237,7 @@
           configuration,
           getMode(),
           getMinApiLevel(),
+          getDiagnosticsHandler(),
           useTreeShaking,
           useDiscardedChecker,
           useMinification,
@@ -392,12 +393,13 @@
       ProguardConfiguration proguardConfiguration,
       CompilationMode mode,
       int minApiLevel,
+      DiagnosticsHandler diagnosticsHandler,
       boolean useTreeShaking,
       boolean useDiscardedChecker,
       boolean useMinification,
       boolean ignoreMissingClasses,
       Path proguardMapOutput) {
-    super(inputApp, outputPath, outputMode, mode, minApiLevel);
+    super(inputApp, outputPath, outputMode, mode, minApiLevel, diagnosticsHandler);
     assert proguardConfiguration != null;
     assert mainDexKeepRules != null;
     assert getOutputMode() == OutputMode.Indexed : "Only regular mode is supported in R8";
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index 2ba4421..4405ef4 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -101,7 +101,7 @@
     this.lambdaRewriter = enableDesugaring ? new LambdaRewriter(this) : null;
     this.interfaceMethodRewriter =
         (enableDesugaring && enableInterfaceMethodDesugaring())
-            ? new InterfaceMethodRewriter(this) : null;
+            ? new InterfaceMethodRewriter(this, options) : null;
     if (enableWholeProgramOptimizations) {
       assert appInfo.hasSubtyping();
       this.inliner = new Inliner(appInfo.withSubtyping(), graphLense, options);
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 99417c3..c5545f5 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
@@ -6,7 +6,6 @@
 
 import com.android.tools.r8.ApiLevelException;
 import com.android.tools.r8.Resource;
-import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.graph.DexApplication.Builder;
 import com.android.tools.r8.graph.DexCallSite;
@@ -26,7 +25,8 @@
 import com.android.tools.r8.ir.code.InvokeStatic;
 import com.android.tools.r8.ir.code.InvokeSuper;
 import com.android.tools.r8.ir.conversion.IRConverter;
-import com.android.tools.r8.logging.Log;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.StringDiagnostic;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import java.util.ListIterator;
@@ -65,6 +65,7 @@
   private static final String DEFAULT_METHOD_PREFIX = "$default$";
 
   private final IRConverter converter;
+  private final InternalOptions options;
   final DexItemFactory factory;
 
   // All forwarding methods generated during desugaring. We don't synchronize access
@@ -84,10 +85,11 @@
     ExcludeDexResources
   }
 
-  public InterfaceMethodRewriter(IRConverter converter) {
+  public InterfaceMethodRewriter(IRConverter converter, InternalOptions options) {
     assert converter != null;
     this.converter = converter;
-    this.factory = converter.application.dexItemFactory;
+    this.options = options;
+    this.factory = options.itemFactory;
   }
 
   // Rewrites the references to static and default interface methods.
@@ -298,7 +300,7 @@
     // TODO replace by a proper warning mechanic (see b/65154707).
     // TODO think about using a common deduplicating mechanic with Enqueuer
     if (reportedMissing.add(missing)) {
-      System.err.println(message);
+      options.diagnosticsHandler.warning(new StringDiagnostic(message));
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/utils/DefaultDiagnosticsHandler.java b/src/main/java/com/android/tools/r8/utils/DefaultDiagnosticsHandler.java
new file mode 100644
index 0000000..68c2840
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/DefaultDiagnosticsHandler.java
@@ -0,0 +1,9 @@
+// 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.utils;
+
+import com.android.tools.r8.DiagnosticsHandler;
+
+public class DefaultDiagnosticsHandler implements DiagnosticsHandler {
+}
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 f497ce5..a72d356 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils;
 
-import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.dex.Marker;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.InvalidDebugInfoException;
@@ -126,6 +126,8 @@
 
   public Path proguardMapOutput = null;
 
+  public DiagnosticsHandler diagnosticsHandler = new DefaultDiagnosticsHandler();
+
   public void warningInvalidDebugInfo(DexEncodedMethod method, InvalidDebugInfoException e) {
     warningInvalidDebugInfoCount++;
   }
diff --git a/src/main/java/com/android/tools/r8/utils/StringDiagnostic.java b/src/main/java/com/android/tools/r8/utils/StringDiagnostic.java
new file mode 100644
index 0000000..0606be2
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/StringDiagnostic.java
@@ -0,0 +1,19 @@
+// 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.utils;
+
+import com.android.tools.r8.Diagnostic;
+
+public class StringDiagnostic implements Diagnostic {
+  private final String message;
+
+  public StringDiagnostic(String message) {
+    this.message = message;
+  }
+
+  @Override
+  public String toString() {
+    return message;
+  }
+}