Command line options for rewriting assertions to invoke assertion handler

Bug: 209445989
Change-Id: I2d2a232859f59855fd97b64b7e78c4eba0457cfc
diff --git a/src/main/java/com/android/tools/r8/BaseCompilerCommandParser.java b/src/main/java/com/android/tools/r8/BaseCompilerCommandParser.java
index 1316be8..631b5b3 100644
--- a/src/main/java/com/android/tools/r8/BaseCompilerCommandParser.java
+++ b/src/main/java/com/android/tools/r8/BaseCompilerCommandParser.java
@@ -3,8 +3,11 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
-import com.android.tools.r8.AssertionsConfiguration.AssertionTransformation;
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.ExceptionDiagnostic;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringDiagnostic;
@@ -38,7 +41,16 @@
           "  --force-pa[:[<class name>|<package name>...]]",
           "                          # Don't change javac generated assertion code. This",
           "                          # is the default handling of javac assertion code when",
-          "                          # generating class file format.");
+          "                          # generating class file format.",
+          "  --force-assertions-handler:<handler method>[:[<class name>|<package name>...]]",
+          "  --force-ah:<handler method>[:[<class name>|<package name>...]]",
+          "                          # Change javac and kotlinc generated assertion code to invoke",
+          "                          # the method <handler method> with each assertion error",
+          "                          # instead of throwing it. The <handler method> is specified"
+              + " as",
+          "                          # a class name followed by a dot and the method name. The",
+          "                          # handler method must take a single argument of type",
+          "                          # java.lang.Throwable and have return type void.");
 
   static final Iterable<String> THREAD_COUNT_USAGE_MESSAGE =
       Arrays.asList(
@@ -82,23 +94,54 @@
 
   private static String PACKAGE_ASSERTION_POSTFIX = "...";
 
+  private enum AssertionTransformationType {
+    ENABLE,
+    DISABLE,
+    PASSTHROUGH,
+    HANDLER
+  }
+
+  private AssertionsConfiguration.Builder prepareBuilderForScope(
+      AssertionsConfiguration.Builder builder,
+      AssertionTransformationType transformation,
+      MethodReference assertionHandler) {
+    switch (transformation) {
+      case ENABLE:
+        return builder.setCompileTimeEnable();
+      case DISABLE:
+        return builder.setCompileTimeDisable();
+      case PASSTHROUGH:
+        return builder.setPassthrough();
+      case HANDLER:
+        return builder.setAssertionHandler(assertionHandler);
+      default:
+        throw new Unreachable();
+    }
+  }
+
   private void addAssertionTransformation(
-      B builder, AssertionTransformation transformation, String scope) {
+      B builder,
+      AssertionTransformationType transformation,
+      MethodReference assertionHandler,
+      String scope) {
     if (scope == null) {
       builder.addAssertionsConfiguration(
-          b -> b.setTransformation(transformation).setScopeAll().build());
+          b -> prepareBuilderForScope(b, transformation, assertionHandler).setScopeAll().build());
     } else {
       assert scope.length() > 0;
       if (scope.endsWith(PACKAGE_ASSERTION_POSTFIX)) {
         builder.addAssertionsConfiguration(
             b ->
-                b.setTransformation(transformation)
+                prepareBuilderForScope(b, transformation, assertionHandler)
                     .setScopePackage(
                         scope.substring(0, scope.length() - PACKAGE_ASSERTION_POSTFIX.length()))
                     .build());
       } else {
         builder.addAssertionsConfiguration(
-            b -> b.setTransformation(transformation).setScopeClass(scope).build());
+            b ->
+                prepareBuilderForScope(b, transformation, assertionHandler)
+                    .setScopeClass(scope)
+                    .build());
       }
     }
   }
@@ -110,34 +153,78 @@
     String FORCE_DA = "--force-da";
     String FORCE_PASSTHROUGH_ASSERTIONS = "--force-passthrough-assertions";
     String FORCE_PA = "--force-pa";
+    String FORCE_ASSERTIONS_HANDLER = "--force-assertions-handler";
+    String FORCE_AH = "--force-ah";
 
-    AssertionTransformation transformation = null;
+    AssertionTransformationType transformation = null;
+    MethodReference assertionsHandler = null;
     String remaining = null;
     if (arg.startsWith(FORCE_ENABLE_ASSERTIONS)) {
-      transformation = AssertionTransformation.ENABLE;
+      transformation = AssertionTransformationType.ENABLE;
       remaining = arg.substring(FORCE_ENABLE_ASSERTIONS.length());
     } else if (arg.startsWith(FORCE_EA)) {
-      transformation = AssertionTransformation.ENABLE;
+      transformation = AssertionTransformationType.ENABLE;
       remaining = arg.substring(FORCE_EA.length());
     } else if (arg.startsWith(FORCE_DISABLE_ASSERTIONS)) {
-      transformation = AssertionTransformation.DISABLE;
+      transformation = AssertionTransformationType.DISABLE;
       remaining = arg.substring(FORCE_DISABLE_ASSERTIONS.length());
     } else if (arg.startsWith(FORCE_DA)) {
-      transformation = AssertionTransformation.DISABLE;
+      transformation = AssertionTransformationType.DISABLE;
       remaining = arg.substring(FORCE_DA.length());
     } else if (arg.startsWith(FORCE_PASSTHROUGH_ASSERTIONS)) {
-      transformation = AssertionTransformation.PASSTHROUGH;
+      transformation = AssertionTransformationType.PASSTHROUGH;
       remaining = arg.substring(FORCE_PASSTHROUGH_ASSERTIONS.length());
     } else if (arg.startsWith(FORCE_PA)) {
-      transformation = AssertionTransformation.PASSTHROUGH;
+      transformation = AssertionTransformationType.PASSTHROUGH;
       remaining = arg.substring(FORCE_PA.length());
+    } else if (arg.startsWith(FORCE_ASSERTIONS_HANDLER)) {
+      transformation = AssertionTransformationType.HANDLER;
+      remaining = arg.substring(FORCE_ASSERTIONS_HANDLER.length());
+    } else if (arg.startsWith(FORCE_AH)) {
+      transformation = AssertionTransformationType.HANDLER;
+      remaining = arg.substring(FORCE_AH.length());
+    }
+    if (transformation == AssertionTransformationType.HANDLER) {
+      if (remaining.length() == 0 || (remaining.length() == 1 && remaining.charAt(0) == ':')) {
+        throw builder.fatalError(
+            new StringDiagnostic("Missing required argument <handler method>", origin));
+      }
+      if (remaining.charAt(0) != ':') {
+        return false;
+      }
+      remaining = remaining.substring(1);
+      int index = remaining.indexOf(':');
+      if (index == 0) {
+        throw builder.fatalError(
+            new StringDiagnostic("Missing required argument <handler method>", origin));
+      }
+      String assertionsHandlerString = index > 0 ? remaining.substring(0, index) : remaining;
+      int lastDotIndex = assertionsHandlerString.lastIndexOf('.');
+      if (assertionsHandlerString.length() < 3
+          || lastDotIndex <= 0
+          || lastDotIndex == assertionsHandlerString.length() - 1
+          || !DescriptorUtils.isValidJavaType(assertionsHandlerString.substring(0, lastDotIndex))) {
+        throw builder.fatalError(
+            new StringDiagnostic(
+                "Invalid argument <handler method>: " + assertionsHandlerString, origin));
+      }
+      assertionsHandler =
+          Reference.methodFromDescriptor(
+              DescriptorUtils.javaTypeToDescriptor(
+                  assertionsHandlerString.substring(0, lastDotIndex)),
+              assertionsHandlerString.substring(lastDotIndex + 1),
+              "(Ljava/lang/Throwable;)V");
+      remaining = remaining.substring(assertionsHandlerString.length());
     }
     if (transformation != null) {
       if (remaining.length() == 0) {
-        addAssertionTransformation(builder, transformation, null);
+        addAssertionTransformation(builder, transformation, assertionsHandler, null);
         return true;
       } else {
-        if (remaining.length() == 1 || remaining.charAt(0) != ':') {
+        if (remaining.length() == 1 && remaining.charAt(0) == ':') {
+          throw builder.fatalError(new StringDiagnostic("Missing optional argument", origin));
+        }
+        if (remaining.charAt(0) != ':') {
           return false;
         }
         String classOrPackageScope = remaining.substring(1);
@@ -147,7 +234,8 @@
           builder.error(
               new StringDiagnostic("Illegal assertion scope: " + classOrPackageScope, origin));
         }
-        addAssertionTransformation(builder, transformation, remaining.substring(1));
+        addAssertionTransformation(
+            builder, transformation, assertionsHandler, remaining.substring(1));
         return true;
       }
     } else {
diff --git a/src/test/java/com/android/tools/r8/D8CommandTest.java b/src/test/java/com/android/tools/r8/D8CommandTest.java
index cf095b1..7a8c4e4 100644
--- a/src/test/java/com/android/tools/r8/D8CommandTest.java
+++ b/src/test/java/com/android/tools/r8/D8CommandTest.java
@@ -22,6 +22,7 @@
 import com.android.tools.r8.dex.Marker.Tool;
 import com.android.tools.r8.origin.EmbeddedOrigin;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.FileUtils;
@@ -36,6 +37,7 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Set;
+import java.util.function.Function;
 import java.util.zip.ZipFile;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -586,6 +588,13 @@
     assertEquals(AssertionTransformationScope.ALL, entries.get(0).getScope());
   }
 
+  private void checkSingleForceAllAssertion(
+      List<AssertionsConfiguration> entries, Function<AssertionsConfiguration, Boolean> check) {
+    assertEquals(1, entries.size());
+    assertTrue(check.apply(entries.get(0)));
+    assertEquals(AssertionTransformationScope.ALL, entries.get(0).getScope());
+  }
+
   private void checkSingleForceClassAndPackageAssertion(
       List<AssertionsConfiguration> entries, AssertionTransformation transformation) {
     assertEquals(2, entries.size());
@@ -597,6 +606,19 @@
     assertEquals("PackageName", entries.get(1).getValue());
   }
 
+  private void checkSingleForceClassAndPackageAssertion(
+      List<AssertionsConfiguration> entries,
+      Function<AssertionsConfiguration, Boolean> checkClass,
+      Function<AssertionsConfiguration, Boolean> checkPackage) {
+    assertEquals(2, entries.size());
+    assertTrue(checkClass.apply(entries.get(0)));
+    assertEquals(AssertionTransformationScope.CLASS, entries.get(0).getScope());
+    assertEquals("ClassName", entries.get(0).getValue());
+    assertTrue(checkPackage.apply(entries.get(1)));
+    assertEquals(AssertionTransformationScope.PACKAGE, entries.get(1).getScope());
+    assertEquals("PackageName", entries.get(1).getValue());
+  }
+
   @Test
   public void forceAssertionOption() throws Exception {
     checkSingleForceAllAssertion(
@@ -622,6 +644,47 @@
                 "--force-passthrough-assertions:PackageName...")
             .getAssertionsConfiguration(),
         AssertionTransformation.PASSTHROUGH);
+    checkSingleForceAllAssertion(
+        parse("--force-assertions-handler:com.example.MyHandler.handler")
+            .getAssertionsConfiguration(),
+        configuration ->
+            configuration.isAssertionHandler()
+                && configuration
+                    .getAssertionHandler()
+                    .getHolderClass()
+                    .equals(Reference.classFromDescriptor("Lcom/example/MyHandler;"))
+                && configuration.getAssertionHandler().getMethodName().equals("handler")
+                && configuration
+                    .getAssertionHandler()
+                    .getMethodDescriptor()
+                    .equals("(Ljava/lang/Throwable;)V"));
+    checkSingleForceClassAndPackageAssertion(
+        parse(
+                "--force-assertions-handler:com.example.MyHandler.handler1:ClassName",
+                "--force-assertions-handler:com.example.MyHandler.handler2:PackageName...")
+            .getAssertionsConfiguration(),
+        configuration ->
+            configuration.isAssertionHandler()
+                && configuration
+                    .getAssertionHandler()
+                    .getHolderClass()
+                    .equals(Reference.classFromDescriptor("Lcom/example/MyHandler;"))
+                && configuration.getAssertionHandler().getMethodName().equals("handler1")
+                && configuration
+                    .getAssertionHandler()
+                    .getMethodDescriptor()
+                    .equals("(Ljava/lang/Throwable;)V"),
+        configuration ->
+            configuration.isAssertionHandler()
+                && configuration
+                    .getAssertionHandler()
+                    .getHolderClass()
+                    .equals(Reference.classFromDescriptor("Lcom/example/MyHandler;"))
+                && configuration.getAssertionHandler().getMethodName().equals("handler2")
+                && configuration
+                    .getAssertionHandler()
+                    .getMethodDescriptor()
+                    .equals("(Ljava/lang/Throwable;)V"));
   }
 
   @Test(expected = CompilationFailedException.class)
diff --git a/src/test/java/com/android/tools/r8/L8CommandTest.java b/src/test/java/com/android/tools/r8/L8CommandTest.java
index 358d863..2e54294 100644
--- a/src/test/java/com/android/tools/r8/L8CommandTest.java
+++ b/src/test/java/com/android/tools/r8/L8CommandTest.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.dex.Marker.Tool;
 import com.android.tools.r8.origin.EmbeddedOrigin;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions;
@@ -30,6 +31,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.function.Function;
 import org.hamcrest.Matcher;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -375,6 +377,13 @@
     assertEquals(AssertionTransformationScope.ALL, entries.get(0).getScope());
   }
 
+  private void checkSingleForceAllAssertion(
+      List<AssertionsConfiguration> entries, Function<AssertionsConfiguration, Boolean> check) {
+    assertEquals(1, entries.size());
+    assertTrue(check.apply(entries.get(0)));
+    assertEquals(AssertionTransformationScope.ALL, entries.get(0).getScope());
+  }
+
   private void checkSingleForceClassAndPackageAssertion(
       List<AssertionsConfiguration> entries, AssertionTransformation transformation) {
     assertEquals(2, entries.size());
@@ -386,6 +395,19 @@
     assertEquals("PackageName", entries.get(1).getValue());
   }
 
+  private void checkSingleForceClassAndPackageAssertion(
+      List<AssertionsConfiguration> entries,
+      Function<AssertionsConfiguration, Boolean> checkClass,
+      Function<AssertionsConfiguration, Boolean> checkPackage) {
+    assertEquals(2, entries.size());
+    assertTrue(checkClass.apply(entries.get(0)));
+    assertEquals(AssertionTransformationScope.CLASS, entries.get(0).getScope());
+    assertEquals("ClassName", entries.get(0).getValue());
+    assertTrue(checkPackage.apply(entries.get(1)));
+    assertEquals(AssertionTransformationScope.PACKAGE, entries.get(1).getScope());
+    assertEquals("PackageName", entries.get(1).getValue());
+  }
+
   @Test
   public void forceAssertionOption() throws Exception {
     checkSingleForceAllAssertion(
@@ -433,6 +455,52 @@
                 ToolHelper.getDesugarLibJsonForTesting().toString())
             .getAssertionsConfiguration(),
         AssertionTransformation.PASSTHROUGH);
+    checkSingleForceAllAssertion(
+        parse(
+                "--force-assertions-handler:com.example.MyHandler.handler",
+                "--desugared-lib",
+                ToolHelper.getDesugarLibJsonForTesting().toString())
+            .getAssertionsConfiguration(),
+        configuration ->
+            configuration.isAssertionHandler()
+                && configuration
+                    .getAssertionHandler()
+                    .getHolderClass()
+                    .equals(Reference.classFromDescriptor("Lcom/example/MyHandler;"))
+                && configuration.getAssertionHandler().getMethodName().equals("handler")
+                && configuration
+                    .getAssertionHandler()
+                    .getMethodDescriptor()
+                    .equals("(Ljava/lang/Throwable;)V"));
+    checkSingleForceClassAndPackageAssertion(
+        parse(
+                "--force-assertions-handler:com.example.MyHandler.handler1:ClassName",
+                "--force-assertions-handler:com.example.MyHandler.handler2:PackageName...",
+                "--desugared-lib",
+                ToolHelper.getDesugarLibJsonForTesting().toString())
+            .getAssertionsConfiguration(),
+        configuration ->
+            configuration.isAssertionHandler()
+                && configuration
+                    .getAssertionHandler()
+                    .getHolderClass()
+                    .equals(Reference.classFromDescriptor("Lcom/example/MyHandler;"))
+                && configuration.getAssertionHandler().getMethodName().equals("handler1")
+                && configuration
+                    .getAssertionHandler()
+                    .getMethodDescriptor()
+                    .equals("(Ljava/lang/Throwable;)V"),
+        configuration ->
+            configuration.isAssertionHandler()
+                && configuration
+                    .getAssertionHandler()
+                    .getHolderClass()
+                    .equals(Reference.classFromDescriptor("Lcom/example/MyHandler;"))
+                && configuration.getAssertionHandler().getMethodName().equals("handler2")
+                && configuration
+                    .getAssertionHandler()
+                    .getMethodDescriptor()
+                    .equals("(Ljava/lang/Throwable;)V"));
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/R8CommandTest.java b/src/test/java/com/android/tools/r8/R8CommandTest.java
index 325344f..536f5bb 100644
--- a/src/test/java/com/android/tools/r8/R8CommandTest.java
+++ b/src/test/java/com/android/tools/r8/R8CommandTest.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.dex.Marker.Tool;
 import com.android.tools.r8.origin.EmbeddedOrigin;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.ThreadUtils;
@@ -36,6 +37,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.function.Function;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 import java.util.zip.ZipOutputStream;
@@ -717,6 +719,13 @@
     assertEquals(AssertionTransformationScope.ALL, entries.get(0).getScope());
   }
 
+  private void checkSingleForceAllAssertion(
+      List<AssertionsConfiguration> entries, Function<AssertionsConfiguration, Boolean> check) {
+    assertEquals(1, entries.size());
+    assertTrue(check.apply(entries.get(0)));
+    assertEquals(AssertionTransformationScope.ALL, entries.get(0).getScope());
+  }
+
   private void checkSingleForceClassAndPackageAssertion(
       List<AssertionsConfiguration> entries, AssertionTransformation transformation) {
     assertEquals(2, entries.size());
@@ -728,6 +737,19 @@
     assertEquals("PackageName", entries.get(1).getValue());
   }
 
+  private void checkSingleForceClassAndPackageAssertion(
+      List<AssertionsConfiguration> entries,
+      Function<AssertionsConfiguration, Boolean> checkClass,
+      Function<AssertionsConfiguration, Boolean> checkPackage) {
+    assertEquals(2, entries.size());
+    assertTrue(checkClass.apply(entries.get(0)));
+    assertEquals(AssertionTransformationScope.CLASS, entries.get(0).getScope());
+    assertEquals("ClassName", entries.get(0).getValue());
+    assertTrue(checkPackage.apply(entries.get(1)));
+    assertEquals(AssertionTransformationScope.PACKAGE, entries.get(1).getScope());
+    assertEquals("PackageName", entries.get(1).getValue());
+  }
+
   @Test
   public void forceAssertionOption() throws Exception {
     checkSingleForceAllAssertion(
@@ -753,6 +775,47 @@
                 "--force-passthrough-assertions:PackageName...")
             .getAssertionsConfiguration(),
         AssertionTransformation.PASSTHROUGH);
+    checkSingleForceAllAssertion(
+        parse("--force-assertions-handler:com.example.MyHandler.handler")
+            .getAssertionsConfiguration(),
+        configuration ->
+            configuration.isAssertionHandler()
+                && configuration
+                    .getAssertionHandler()
+                    .getHolderClass()
+                    .equals(Reference.classFromDescriptor("Lcom/example/MyHandler;"))
+                && configuration.getAssertionHandler().getMethodName().equals("handler")
+                && configuration
+                    .getAssertionHandler()
+                    .getMethodDescriptor()
+                    .equals("(Ljava/lang/Throwable;)V"));
+    checkSingleForceClassAndPackageAssertion(
+        parse(
+                "--force-assertions-handler:com.example.MyHandler.handler1:ClassName",
+                "--force-assertions-handler:com.example.MyHandler.handler2:PackageName...")
+            .getAssertionsConfiguration(),
+        configuration ->
+            configuration.isAssertionHandler()
+                && configuration
+                    .getAssertionHandler()
+                    .getHolderClass()
+                    .equals(Reference.classFromDescriptor("Lcom/example/MyHandler;"))
+                && configuration.getAssertionHandler().getMethodName().equals("handler1")
+                && configuration
+                    .getAssertionHandler()
+                    .getMethodDescriptor()
+                    .equals("(Ljava/lang/Throwable;)V"),
+        configuration ->
+            configuration.isAssertionHandler()
+                && configuration
+                    .getAssertionHandler()
+                    .getHolderClass()
+                    .equals(Reference.classFromDescriptor("Lcom/example/MyHandler;"))
+                && configuration.getAssertionHandler().getMethodName().equals("handler2")
+                && configuration
+                    .getAssertionHandler()
+                    .getMethodDescriptor()
+                    .equals("(Ljava/lang/Throwable;)V"));
   }
 
   @Test(expected = CompilationFailedException.class)