Desugaring of try-with-resources.
Adds desugaring of try-with-resources for pre-19 min-sdk
version, it essentially boils down to removing all calls to
Throwable::addSuppressed(...) and replacing all calls to
Throwable::getSuppressed() with empty array of Throwable.
The desugaring requires library and class path since it needs
to inspect/check exception class hierarchy. Disabling it by
default until we measure performance impact.
Bug:37744723
BUG=
Change-Id: If2781ec68cab3a2b38127ee20fffb94ceac3157a
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 48d8690..a3d92ac 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -143,6 +143,7 @@
assert internal.useTreeShaking;
internal.useTreeShaking = false;
assert internal.interfaceMethodDesugaring == OffOrAuto.Off;
+ assert internal.tryWithResourcesDesugaring == OffOrAuto.Off;
assert internal.allowAccessModification;
internal.allowAccessModification = false;
assert internal.inlineAccessors;
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 5bff70d..299be81 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -86,6 +86,7 @@
public DexString stringDescriptor = createString("Ljava/lang/String;");
public DexString objectDescriptor = createString("Ljava/lang/Object;");
public DexString classDescriptor = createString("Ljava/lang/Class;");
+ public DexString throwableDescriptor = createString("Ljava/lang/Throwable;");
public DexString objectsDescriptor = createString("Ljava/util/Objects;");
public DexString constructorMethodName = createString(Constants.INSTANCE_INITIALIZER_NAME);
@@ -94,6 +95,7 @@
public DexString thisName = createString("this");
private DexString charArrayDescriptor = createString("[C");
+ public DexString throwableArrayDescriptor = createString("[Ljava/lang/Throwable;");
public DexType booleanType = createType(booleanDescriptor);
public DexType byteType = createType(byteDescriptor);
@@ -117,6 +119,7 @@
public DexType stringType = createType(stringDescriptor);
public DexType objectType = createType(objectDescriptor);
+ public DexType throwableType = createType(throwableDescriptor);
public StringBuildingMethods stringBuilderMethods =
new StringBuildingMethods(createString("Ljava/lang/StringBuilder;"));
@@ -125,6 +128,7 @@
public ObjectsMethods objectsMethods = new ObjectsMethods();
public ObjectMethods objectMethods = new ObjectMethods();
public LongMethods longMethods = new LongMethods();
+ public ThrowableMethods throwableMethods = new ThrowableMethods();
public void clearSubtypeInformation() {
types.values().forEach(DexType::clearSubtypeInformation);
@@ -139,6 +143,18 @@
}
}
+ public class ThrowableMethods {
+ public final DexMethod addSuppressed;
+ public final DexMethod getSuppressed;
+
+ private ThrowableMethods() {
+ addSuppressed = createMethod(throwableDescriptor,
+ createString("addSuppressed"), voidDescriptor, new DexString[] { throwableDescriptor });
+ getSuppressed = createMethod(throwableDescriptor,
+ createString("getSuppressed"), throwableArrayDescriptor, DexString.EMPTY_ARRAY);
+ }
+ }
+
public class ObjectMethods {
public DexMethod getClass;
@@ -410,18 +426,18 @@
private DexString shorty(DexType returnType, DexType[] parameters) {
StringBuilder builder = new StringBuilder();
- builder.append(returnType.toDescriptorString().charAt(0));
+ addToShorty(builder, returnType);
for (DexType parameter : parameters) {
- String descriptor = parameter.toDescriptorString();
- if (descriptor.charAt(0) == '[') {
- builder.append('L');
- } else {
- builder.append(descriptor.charAt(0));
- }
+ addToShorty(builder, parameter);
}
return createString(builder.toString());
}
+ private void addToShorty(StringBuilder builder, DexType type) {
+ char first = type.toDescriptorString().charAt(0);
+ builder.append(first == '[' ? 'L' : first);
+ }
+
private static <S extends PresortedComparable<S>> void assignSortedIndices(Collection<S> items,
NamingLens namingLens) {
List<S> sorted = new ArrayList<>(items);
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 fa7ee4d..22ce470 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
@@ -132,6 +132,16 @@
throw new Unreachable();
}
+ private boolean enableTryWithResourcesDesugaring() {
+ switch (options.tryWithResourcesDesugaring) {
+ case Off:
+ return false;
+ case Auto:
+ return !options.canUseSuppressedExceptions();
+ }
+ throw new Unreachable();
+ }
+
private void markLibraryMethodsReturningReceiver() {
DexItemFactory dexItemFactory = appInfo.dexItemFactory;
dexItemFactory.stringBuilderMethods.forEeachAppendMethod(this::markReturnsReceiver);
@@ -427,6 +437,10 @@
DeadCodeRemover.removeDeadCode(code, codeRewriter, options);
assert code.isConsistentSSA();
+ if (enableTryWithResourcesDesugaring()) {
+ codeRewriter.rewriteThrowableAddAndGetSuppressed(code);
+ }
+
if (lambdaRewriter != null) {
lambdaRewriter.desugarLambdas(method, code);
assert code.isConsistentSSA();
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 5e701ed..81a9e55 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
@@ -5,7 +5,9 @@
package com.android.tools.r8.ir.optimize;
import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexMethod;
@@ -17,6 +19,7 @@
import com.android.tools.r8.ir.code.Cmp;
import com.android.tools.r8.ir.code.Cmp.Bias;
import com.android.tools.r8.ir.code.ConstNumber;
+import com.android.tools.r8.ir.code.ConstType;
import com.android.tools.r8.ir.code.DominatorTree;
import com.android.tools.r8.ir.code.Goto;
import com.android.tools.r8.ir.code.IRCode;
@@ -938,4 +941,88 @@
}
assert code.isConsistentSSA();
}
+
+ // Removes calls to Throwable.addSuppressed(Throwable) and rewrites
+ // Throwable.getSuppressed() into new Throwable[0].
+ //
+ // Note that addSuppressed() and getSuppressed() methods are final in
+ // Throwable, so these changes don't have to worry about overrides.
+ public void rewriteThrowableAddAndGetSuppressed(IRCode code) {
+ boolean removeUnneededCatchHandlers = false;
+ DexItemFactory.ThrowableMethods throwableMethods = dexItemFactory.throwableMethods;
+
+ for (BasicBlock block : code.blocks) {
+ InstructionListIterator iterator = block.listIterator();
+ while (iterator.hasNext()) {
+ Instruction current = iterator.next();
+ if (current.isInvokeMethod()) {
+ DexMethod invokedMethod = current.asInvokeMethod().getInvokedMethod();
+
+ if (matchesMethodOfThrowable(invokedMethod, throwableMethods.addSuppressed)) {
+ // Remove Throwable::addSuppressed(Throwable) call.
+ iterator.remove();
+ removeUnneededCatchHandlers = true;
+
+ } else if (matchesMethodOfThrowable(invokedMethod, throwableMethods.getSuppressed)) {
+ Value destValue = current.outValue();
+ if (destValue == null) {
+ // If the result of the call was not used we don't create
+ // an empty array and just remove the call.
+ iterator.remove();
+ removeUnneededCatchHandlers = true;
+ continue;
+ }
+
+ // Replace call to Throwable::getSuppressed() with new Throwable[0].
+
+ // First insert the constant value *before* the current instruction.
+ Value zero = new Value(code.valueNumberGenerator.next(), -1, MoveType.SINGLE, null);
+ assert iterator.hasPrevious();
+ iterator.previous();
+ iterator.add(new ConstNumber(ConstType.INT, zero, 0));
+
+ // Then replace the invoke instruction with NewArrayEmpty instruction.
+ Instruction next = iterator.next();
+ assert current == next;
+ NewArrayEmpty newArray = new NewArrayEmpty(destValue, zero,
+ dexItemFactory.createType(dexItemFactory.throwableArrayDescriptor));
+ iterator.replaceCurrentInstruction(newArray);
+
+ // NOTE: nothing needs to be changed in catch handlers since we replace
+ // one throwable instruction with another.
+ }
+ }
+ }
+ }
+
+ // If at least one addSuppressed(...) call was removed, or we were able
+ // to remove getSuppressed() call without replacing it with a new empty array,
+ // we need to deal with possible unreachable catch handlers.
+ if (removeUnneededCatchHandlers) {
+ removeUnneededCatchHandlers(code);
+ }
+
+ assert code.isConsistentSSA();
+ }
+
+ private boolean matchesMethodOfThrowable(DexMethod invoked, DexMethod expected) {
+ return invoked.name == expected.name
+ && invoked.proto == expected.proto
+ && isSubtypeOfThrowable(invoked.holder);
+ }
+
+ private boolean isSubtypeOfThrowable(DexType type) {
+ while (type != null && type != dexItemFactory.objectType) {
+ if (type == dexItemFactory.throwableType) {
+ return true;
+ }
+ DexClass dexClass = appInfo.definitionFor(type);
+ if (dexClass == null) {
+ throw new CompilationError("Class or interface " + type.toSourceString() +
+ " required for desugaring of try-with-resources is not found.");
+ }
+ type = dexClass.superType;
+ }
+ return false;
+ }
}
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 1fd72f8..f80f947 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -54,6 +54,8 @@
// Defines interface method rewriter behavior.
public OffOrAuto interfaceMethodDesugaring = OffOrAuto.Off;
+ // Defines try-with-resources rewriter behavior.
+ public OffOrAuto tryWithResourcesDesugaring = OffOrAuto.Off;
public boolean useTreeShaking = true;
@@ -254,6 +256,10 @@
return minApiLevel >= Constants.ANDROID_K_API;
}
+ public boolean canUseSuppressedExceptions() {
+ return minApiLevel >= Constants.ANDROID_K_API;
+ }
+
public boolean canUsePrivateInterfaceMethods() {
return minApiLevel >= Constants.ANDROID_N_API;
}
diff --git a/src/test/examplesAndroidO/trywithresources/TryWithResources.java b/src/test/examplesAndroidO/trywithresources/TryWithResources.java
new file mode 100644
index 0000000..ac779d6
--- /dev/null
+++ b/src/test/examplesAndroidO/trywithresources/TryWithResources.java
@@ -0,0 +1,237 @@
+// 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 trywithresources;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+public abstract class TryWithResources {
+ // --- TEST SUPPORT ---
+
+ interface Test {
+ void test() throws Throwable;
+ }
+
+ private void test(Test test) {
+ try {
+ test.test();
+ } catch (Throwable e) {
+ dumpException(e);
+ }
+ }
+
+ private void dumpException(Throwable e) {
+ dumpException(e, "Exception: ");
+ }
+
+ private void dumpException(Throwable e, String indent) {
+ assert e != null;
+ System.out.println(indent + e.getMessage());
+
+ indent = indent.replaceAll("[^:]", " ");
+
+ Throwable cause = e.getCause();
+ if (cause != null) {
+ dumpException(cause, indent + " cause: ");
+ }
+
+ // Dump suppressed UNLESS it is a desugared code running
+ // on JVM, in which case we avoid dumping suppressed, since
+ // the output will be used for comparison with desugared code
+ // running on device.
+ if (!desugaredCodeRunningOnJvm()) {
+ Throwable[] suppressed = e.getSuppressed();
+ for (int i = 0; i < suppressed.length; i++) {
+ dumpException(suppressed[i], indent + "supp[" + i + "]: ");
+ }
+ }
+ }
+
+ abstract boolean desugaredCodeRunningOnJvm();
+
+ // --- TEST SYMBOLS ---
+
+ static class Resource implements Closeable {
+ final String tag;
+
+ Resource(String tag) {
+ this.tag = tag;
+ }
+
+ @Override
+ public void close() throws IOException {
+ Class<? extends Resource> cls = this.getClass();
+ System.out.println("Closing " + tag + " (" +
+ cls.getName().substring(TryWithResources.class.getName().length() + 1) + ")");
+ }
+ }
+
+ // --- TEST ---
+
+ class RegularTryWithResources {
+ class RegularResource extends Resource {
+ RegularResource(String tag) {
+ super(tag);
+ }
+ }
+
+ private void test() throws Throwable {
+ test(2);
+ }
+
+ private void test(int level) throws Throwable {
+ try (RegularResource a = new RegularResource("a" + level);
+ RegularResource b = new RegularResource("b" + level)) {
+ if (level > 0) {
+ try {
+ test(level - 1);
+ } catch (Throwable e) {
+ throw new RuntimeException("e" + level, e);
+ }
+ }
+ throw new RuntimeException("primary cause");
+ }
+ }
+ }
+
+ // --- TEST ---
+
+ class FailingTryWithResources {
+ class FailingResource extends Resource {
+ FailingResource(String tag) {
+ super(tag);
+ }
+
+ @Override
+ public void close() throws IOException {
+ super.close();
+ throw new RuntimeException("failed to close '" + tag + "'");
+ }
+ }
+
+ private void test() throws Throwable {
+ test(2);
+ }
+
+ private void test(int level) throws Throwable {
+ try (FailingResource a = new FailingResource("a" + level);
+ FailingResource b = new FailingResource("b" + level)) {
+ if (level > 0) {
+ try {
+ test(level - 1);
+ } catch (Throwable e) {
+ throw new RuntimeException("e" + level, e);
+ }
+ }
+ throw new RuntimeException("primary cause");
+ }
+ }
+ }
+
+ // --- TEST ---
+
+ class ExplicitAddGetSuppressed {
+ class RegularResource extends Resource {
+ RegularResource(String tag) {
+ super(tag);
+ }
+
+ @Override
+ public void close() throws IOException {
+ super.close();
+ throw new RuntimeException("failed to close '" + tag + "'");
+ }
+ }
+
+ private void test() throws Throwable {
+ test(2);
+ }
+
+ private void test(int level) throws RuntimeException {
+ try (RegularResource a = new RegularResource("a" + level);
+ RegularResource b = new RegularResource("b" + level)) {
+ if (level > 0) {
+ try {
+ test(level - 1);
+ } catch (RuntimeException e) {
+ // Just collect suppressed, but throw away the exception.
+ RuntimeException re = new RuntimeException("e" + level);
+ for (Throwable suppressed : e.getSuppressed()) {
+ re.addSuppressed(suppressed);
+ }
+ throw re;
+ }
+ }
+ throw new RuntimeException("primary cause");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ // --- TEST ---
+
+ interface Consumer {
+ void act(RuntimeException re);
+ }
+
+ interface Supplier {
+ Throwable[] get();
+ }
+
+ class AddGetSuppressedRoundTrip {
+ private void test() throws Throwable {
+ RuntimeException carrier = new RuntimeException("carrier");
+ Consumer packer = carrier::addSuppressed;
+ Supplier unpacker = carrier::getSuppressed;
+
+ packer.act(new RuntimeException("original exception A"));
+ packer.act(new RuntimeException("original exception Z"));
+
+ for (Throwable unpacked : unpacker.get()) {
+ if (!desugaredCodeRunningOnJvm()) {
+ dumpException(unpacked);
+ }
+ }
+ }
+ }
+
+ // --- TEST ---
+
+ class UnreachableCatchAfterCallsRemoved {
+ private void test() throws Throwable {
+ RuntimeException main = new RuntimeException("main");
+ RuntimeException origA = new RuntimeException("original exception A");
+ RuntimeException origB = new RuntimeException("original exception Z");
+
+ try {
+ // After both calls below are removed, the whole catch
+ // handler should be removed.
+ main.addSuppressed(origA);
+ main.addSuppressed(origB);
+ } catch (Throwable t) {
+ throw new RuntimeException("UNREACHABLE");
+ }
+
+ // Return value not used.
+ main.getSuppressed();
+ }
+ }
+
+ // --- MAIN TEST ---
+
+ void test() throws Exception {
+ System.out.println("----- TEST 1 -----");
+ test(new RegularTryWithResources()::test);
+ System.out.println("----- TEST 2 -----");
+ test(new FailingTryWithResources()::test);
+ System.out.println("----- TEST 3 -----");
+ test(new ExplicitAddGetSuppressed()::test);
+ System.out.println("----- TEST 4 -----");
+ test(new AddGetSuppressedRoundTrip()::test);
+ System.out.println("----- TEST 5 -----");
+ test(new UnreachableCatchAfterCallsRemoved()::test);
+ System.out.println("------------------");
+ }
+}
diff --git a/src/test/examplesAndroidO/trywithresources/TryWithResourcesDesugaredTests.java b/src/test/examplesAndroidO/trywithresources/TryWithResourcesDesugaredTests.java
new file mode 100644
index 0000000..10a96f9
--- /dev/null
+++ b/src/test/examplesAndroidO/trywithresources/TryWithResourcesDesugaredTests.java
@@ -0,0 +1,24 @@
+// 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 trywithresources;
+
+public class TryWithResourcesDesugaredTests extends TryWithResources {
+ private boolean isAndroid() {
+ try {
+ Class.forName("dalvik.system.VMRuntime");
+ return true;
+ } catch (Exception ignored) {
+ }
+ return false;
+ }
+
+ @Override
+ boolean desugaredCodeRunningOnJvm() {
+ return !isAndroid();
+ }
+
+ public static void main(String[] args) throws Exception {
+ new TryWithResourcesDesugaredTests().test();
+ }
+}
diff --git a/src/test/examplesAndroidO/trywithresources/TryWithResourcesNotDesugaredTests.java b/src/test/examplesAndroidO/trywithresources/TryWithResourcesNotDesugaredTests.java
new file mode 100644
index 0000000..d2a05c3
--- /dev/null
+++ b/src/test/examplesAndroidO/trywithresources/TryWithResourcesNotDesugaredTests.java
@@ -0,0 +1,15 @@
+// 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 trywithresources;
+
+public class TryWithResourcesNotDesugaredTests extends TryWithResources {
+ @Override
+ boolean desugaredCodeRunningOnJvm() {
+ return false;
+ }
+
+ public static void main(String[] args) throws Exception {
+ new TryWithResourcesNotDesugaredTests().test();
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
index 3f63319..457e7f4 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
@@ -13,6 +13,11 @@
import com.android.tools.r8.ToolHelper.DexVm;
import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.FoundClassSubject;
+import com.android.tools.r8.utils.DexInspector.FoundMethodSubject;
+import com.android.tools.r8.utils.DexInspector.InstructionSubject;
+import com.android.tools.r8.utils.DexInspector.InvokeInstructionSubject;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.OffOrAuto;
import com.google.common.collect.ImmutableList;
@@ -20,10 +25,13 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
+import java.util.function.Predicate;
import java.util.function.UnaryOperator;
+import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
@@ -38,6 +46,7 @@
final String mainClass;
final List<Consumer<InternalOptions>> optionConsumers = new ArrayList<>();
+ final List<Consumer<DexInspector>> dexInspectorChecks = new ArrayList<>();
final List<UnaryOperator<B>> builderTransformations = new ArrayList<>();
TestRunner(String testName, String packageName, String mainClass) {
@@ -46,6 +55,35 @@
this.mainClass = mainClass;
}
+ TestRunner withDexCheck(Consumer<DexInspector> check) {
+ dexInspectorChecks.add(check);
+ return this;
+ }
+
+ TestRunner withClassCheck(Consumer<FoundClassSubject> check) {
+ withDexCheck(inspector -> inspector.forAllClasses(check));
+ return this;
+ }
+
+ TestRunner withMethodCheck(Consumer<FoundMethodSubject> check) {
+ withClassCheck(clazz -> clazz.forAllMethods(check));
+ return this;
+ }
+
+ <T extends InstructionSubject> TestRunner
+ withInstructionCheck(Predicate<InstructionSubject> filter, Consumer<T> check) {
+ withMethodCheck(method -> {
+ if (method.isAbstract()) {
+ return;
+ }
+ Iterator<T> iterator = method.iterateInstructions(filter);
+ while (iterator.hasNext()) {
+ check.accept(iterator.next());
+ }
+ });
+ return this;
+ }
+
TestRunner withOptionConsumer(Consumer<InternalOptions> consumer) {
optionConsumers.add(consumer);
return this;
@@ -55,6 +93,10 @@
return withOptionConsumer(o -> o.interfaceMethodDesugaring = behavior);
}
+ TestRunner withTryWithResourcesDesugaring(OffOrAuto behavior) {
+ return withOptionConsumer(o -> o.tryWithResourcesDesugaring = behavior);
+ }
+
TestRunner withBuilderTransformation(UnaryOperator<B> builderTransformation) {
builderTransformations.add(builderTransformation);
return this;
@@ -86,6 +128,13 @@
thrown.expect(Throwable.class);
}
+ if (!dexInspectorChecks.isEmpty()) {
+ DexInspector inspector = new DexInspector(out);
+ for (Consumer<DexInspector> check : dexInspectorChecks) {
+ check.accept(inspector);
+ }
+ }
+
String output = ToolHelper.runArtNoVerificationErrors(out.toString(), qualifiedMainClass);
if (!expectedToFail) {
ToolHelper.ProcessResult javaResult =
@@ -238,5 +287,24 @@
test("repeat_annotations", "repeat_annotations", "RepeatAnnotations").run();
}
+ @Test
+ public void testTryWithResources() throws Throwable {
+ test("try-with-resources-simplified", "trywithresources", "TryWithResourcesNotDesugaredTests")
+ .withTryWithResourcesDesugaring(OffOrAuto.Off)
+ .run();
+ }
+
+ @Test
+ public void testTryWithResourcesDesugared() throws Throwable {
+ test("try-with-resources-simplified", "trywithresources", "TryWithResourcesDesugaredTests")
+ .withTryWithResourcesDesugaring(OffOrAuto.Auto)
+ .withInstructionCheck(InstructionSubject::isInvoke,
+ (InvokeInstructionSubject invoke) -> {
+ Assert.assertFalse(invoke.invokedMethod().name.toString().equals("addSuppressed"));
+ Assert.assertFalse(invoke.invokedMethod().name.toString().equals("getSuppressed"));
+ })
+ .run();
+ }
+
abstract TestRunner test(String testName, String packageName, String mainClass);
}