Desugar two-arg AssertionError constructor on pre-API 19
Turns into a one-arg AssertionError constructor call and a call to initCause. On API 15 and below, a bug in the Apache Harmony implementation of AssertionError prevents propagating the cause so it is dropped.
Bug: 137449279
Test: tools/test.py --dex_vm all --no-internal -v *AssertionErrorRewriteTest*
Change-Id: I3cab39cbf5f94ccf0b2d9c82713ac58af88c7d1b
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index faa7862..7fb524d 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -179,6 +179,7 @@
public final DexString invokeMethodName = createString("invoke");
public final DexString invokeExactMethodName = createString("invokeExact");
+ public final DexString assertionErrorDescriptor = createString("Ljava/lang/AssertionError;");
public final DexString charSequenceDescriptor = createString("Ljava/lang/CharSequence;");
public final DexString charSequenceArrayDescriptor = createString("[Ljava/lang/CharSequence;");
public final DexString stringDescriptor = createString("Ljava/lang/String;");
@@ -316,6 +317,7 @@
public final LongMethods longMethods = new LongMethods();
public final JavaUtilArraysMethods utilArraysMethods = new JavaUtilArraysMethods();
public final ThrowableMethods throwableMethods = new ThrowableMethods();
+ public final AssertionErrorMethods assertionErrorMethods = new AssertionErrorMethods();
public final ClassMethods classMethods = new ClassMethods();
public final ConstructorMethods constructorMethods = new ConstructorMethods();
public final EnumMethods enumMethods = new EnumMethods();
@@ -556,12 +558,29 @@
public final DexMethod addSuppressed;
public final DexMethod getSuppressed;
+ public final DexMethod initCause;
private ThrowableMethods() {
addSuppressed = createMethod(throwableDescriptor,
createString("addSuppressed"), voidDescriptor, new DexString[]{throwableDescriptor});
getSuppressed = createMethod(throwableDescriptor,
createString("getSuppressed"), throwableArrayDescriptor, DexString.EMPTY_ARRAY);
+ initCause = createMethod(throwableDescriptor, createString("initCause"), throwableDescriptor,
+ new DexString[] { throwableDescriptor });
+ }
+ }
+
+ public class AssertionErrorMethods {
+ public final DexMethod initMessage;
+ public final DexMethod initMessageAndCause;
+
+ private AssertionErrorMethods() {
+ this.initMessage =
+ createMethod(assertionErrorDescriptor, constructorMethodName, voidDescriptor,
+ new DexString[] { objectDescriptor });
+ this.initMessageAndCause =
+ createMethod(assertionErrorDescriptor, constructorMethodName, voidDescriptor,
+ new DexString[] { stringDescriptor, throwableDescriptor });
}
}
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 854ed81..36e3b1c 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
@@ -1088,6 +1088,7 @@
}
codeRewriter.rewriteLongCompareAndRequireNonNull(code, options);
+ codeRewriter.rewriteAssertionErrorTwoArgumentConstructor(code, options);
codeRewriter.commonSubexpressionElimination(code);
codeRewriter.simplifyArrayConstruction(code);
codeRewriter.rewriteMoveResult(code);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
index b73c702..6352269 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -126,6 +126,7 @@
import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.Deque;
@@ -3735,6 +3736,43 @@
assert code.isConsistentSSA();
}
+ public void rewriteAssertionErrorTwoArgumentConstructor(IRCode code, InternalOptions options) {
+ if (options.canUseAssertionErrorTwoArgumentConstructor()) {
+ return;
+ }
+
+ InstructionIterator iterator = code.instructionIterator();
+ while (iterator.hasNext()) {
+ Instruction current = iterator.next();
+ if (current.isInvokeMethod()) {
+ DexMethod invokedMethod = current.asInvokeMethod().getInvokedMethod();
+ if (invokedMethod == dexItemFactory.assertionErrorMethods.initMessageAndCause) {
+ // Rewrite calls to new AssertionError(message, cause) to new AssertionError(message)
+ // and then initCause(cause).
+ List<Value> inValues = current.inValues();
+ assert inValues.size() == 3; // receiver, message, cause
+
+ // Remove cause from the constructor call
+ List<Value> newInitInValues = inValues.subList(0, 2);
+ iterator.replaceCurrentInstruction(
+ new InvokeDirect(dexItemFactory.assertionErrorMethods.initMessage, null,
+ newInitInValues));
+
+ // On API 15 and older we cannot use initCause because of a bug in AssertionError.
+ if (options.canInitCauseAfterAssertionErrorObjectConstructor()) {
+ // Add a call to Throwable.initCause(cause)
+ List<Value> initCauseArguments = Arrays.asList(inValues.get(0), inValues.get(2));
+ InvokeVirtual initCause = new InvokeVirtual(dexItemFactory.throwableMethods.initCause,
+ code.createValue(TypeLatticeElement.SINGLE), initCauseArguments);
+ initCause.setPosition(current.getPosition());
+ iterator.add(initCause);
+ }
+ }
+ }
+ }
+ assert code.isConsistentSSA();
+ }
+
/**
* Remove moves that are not actually used by instructions in exiting paths. These moves can arise
* due to debug local info needing a particular value and the live-interval for it then moves it
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 ab297fb..8bed04c 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -1048,6 +1048,20 @@
return isGeneratingClassFiles() || hasMinApi(AndroidApiLevel.K);
}
+ public boolean canUseAssertionErrorTwoArgumentConstructor() {
+ return isGeneratingClassFiles() || hasMinApi(AndroidApiLevel.K);
+ }
+
+ // The Apache Harmony-based AssertionError constructor which takes an Object on API 15 and older
+ // calls the Error supertype constructor with null as the exception cause. This prevents
+ // subsequent calls to initCause() because its implementation checks that cause==this before
+ // allowing a cause to be set.
+ //
+ // https://android.googlesource.com/platform/libcore/+/refs/heads/ics-mr1/luni/src/main/java/java/lang/AssertionError.java#56
+ public boolean canInitCauseAfterAssertionErrorObjectConstructor() {
+ return isGeneratingClassFiles() || hasMinApi(AndroidApiLevel.J);
+ }
+
// Dalvik x86-atom backend had a bug that made it crash on filled-new-array instructions for
// arrays of objects. This is unfortunate, since this never hits arm devices, but we have
// to disallow filled-new-array of objects for dalvik until kitkat. The buggy code was
diff --git a/src/test/java/com/android/tools/r8/GenerateMainDexListTestBuilder.java b/src/test/java/com/android/tools/r8/GenerateMainDexListTestBuilder.java
index 39a7dc6..619f96f 100644
--- a/src/test/java/com/android/tools/r8/GenerateMainDexListTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/GenerateMainDexListTestBuilder.java
@@ -40,7 +40,7 @@
}
@Override
- public GenerateMainDexListRunResult run(TestRuntime runtime, String mainClass)
+ public GenerateMainDexListRunResult run(TestRuntime runtime, String mainClass, String... args)
throws IOException, CompilationFailedException {
throw new Unimplemented("No support for running with a main class");
}
diff --git a/src/test/java/com/android/tools/r8/JvmTestBuilder.java b/src/test/java/com/android/tools/r8/JvmTestBuilder.java
index c4b8b62..c6a5026 100644
--- a/src/test/java/com/android/tools/r8/JvmTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/JvmTestBuilder.java
@@ -9,6 +9,7 @@
import com.android.tools.r8.errors.Unimplemented;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.FileUtils;
+import com.google.common.collect.ObjectArrays;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -43,9 +44,11 @@
return new JvmTestRunResult(builder.build(), result);
}
- public JvmTestRunResult run(TestRuntime runtime, String mainClass) throws IOException {
+ public JvmTestRunResult run(TestRuntime runtime, String mainClass, String... args)
+ throws IOException {
assert runtime.isCf();
- ProcessResult result = ToolHelper.runJava(runtime.asCf().getVm(), classpath, mainClass);
+ ProcessResult result =
+ ToolHelper.runJava(runtime.asCf().getVm(), classpath, ObjectArrays.concat(mainClass, args));
return new JvmTestRunResult(builder.build(), result);
}
diff --git a/src/test/java/com/android/tools/r8/TestBuilder.java b/src/test/java/com/android/tools/r8/TestBuilder.java
index ab6533f..46e3814 100644
--- a/src/test/java/com/android/tools/r8/TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestBuilder.java
@@ -40,7 +40,7 @@
public abstract RR run(String mainClass)
throws CompilationFailedException, ExecutionException, IOException;
- public abstract RR run(TestRuntime runtime, String mainClass)
+ public abstract RR run(TestRuntime runtime, String mainClass, String... args)
throws CompilationFailedException, ExecutionException, IOException;
@Deprecated
@@ -49,9 +49,9 @@
return run(mainClass.getTypeName());
}
- public RR run(TestRuntime runtime, Class<?> mainClass)
+ public RR run(TestRuntime runtime, Class<?> mainClass, String... args)
throws CompilationFailedException, ExecutionException, IOException {
- return run(runtime, mainClass.getTypeName());
+ return run(runtime, mainClass.getTypeName(), args);
}
public abstract DebugTestConfig debugConfig();
diff --git a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
index 1ec471c..ef770d5 100644
--- a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -111,9 +111,9 @@
}
@Override
- public RR run(TestRuntime runtime, String mainClass)
+ public RR run(TestRuntime runtime, String mainClass, String... args)
throws CompilationFailedException, ExecutionException, IOException {
- return compile().run(runtime, mainClass);
+ return compile().run(runtime, mainClass, args);
}
@Override
diff --git a/src/test/java/com/android/tools/r8/rewrite/assertionerror/AssertionErrorRewriteTest.java b/src/test/java/com/android/tools/r8/rewrite/assertionerror/AssertionErrorRewriteTest.java
new file mode 100644
index 0000000..b8ee931
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/rewrite/assertionerror/AssertionErrorRewriteTest.java
@@ -0,0 +1,87 @@
+// Copyright (c) 2019, 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.rewrite.assertionerror;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+import static com.android.tools.r8.ToolHelper.getDefaultAndroidJar;
+import static org.junit.Assume.assumeTrue;
+
+@RunWith(Parameterized.class)
+public class AssertionErrorRewriteTest extends TestBase {
+ @Parameters(name = "{0}")
+ public static Iterable<?> data() {
+ return getTestParameters().withAllRuntimes().build();
+ }
+
+ private final TestParameters parameters;
+ private final boolean expectCause;
+
+ public AssertionErrorRewriteTest(TestParameters parameters) {
+ this.parameters = parameters;
+
+ // The exception cause is only preserved on API 16 and newer.
+ expectCause = parameters.isCfRuntime()
+ || parameters.getRuntime().asDex().getMinApiLevel().getLevel() >= 16;
+ }
+
+ @Test public void d8() throws Exception {
+ assumeTrue(parameters.isDexRuntime());
+
+ testForD8()
+ .addLibraryFiles(getDefaultAndroidJar())
+ .addProgramClasses(Main.class)
+ .setMinApi(parameters.getRuntime())
+ .run(parameters.getRuntime(), Main.class, String.valueOf(expectCause))
+ .assertSuccessWithOutputLines("OK", "OK");
+ }
+
+ @Test public void r8() throws Exception {
+ testForR8(parameters.getBackend())
+ .addLibraryFiles(getDefaultAndroidJar())
+ .addProgramClasses(Main.class)
+ .addKeepMainRule(Main.class)
+ .enableInliningAnnotations()
+ .setMinApi(parameters.getRuntime())
+ .run(parameters.getRuntime(), Main.class, String.valueOf(expectCause))
+ .assertSuccessWithOutputLines("OK", "OK");
+ }
+
+ public static final class Main {
+ public static void main(String[] args) {
+ boolean expectCause = Boolean.parseBoolean(args[0]);
+
+ Throwable expectedCause = new RuntimeException("cause message");
+ try {
+ throwAssertionError(expectedCause);
+ System.out.println("unreachable");
+ } catch (AssertionError e) {
+ String message = e.getMessage();
+ if (!message.equals("message")) {
+ throw new RuntimeException("Incorrect AssertionError message: " + message);
+ } else {
+ System.out.println("OK");
+ }
+
+ Throwable cause = e.getCause();
+ if (expectCause && cause != expectedCause) {
+ throw new RuntimeException("Incorrect AssertionError cause", cause);
+ } else {
+ System.out.println("OK");
+ }
+ }
+ }
+
+ @NeverInline
+ private static void throwAssertionError(Throwable cause) {
+ throw new AssertionError("message", cause);
+ }
+ }
+}