Add reproduction of Lambda desugaring breaking EnclosingMethod
Bug: 158752316
Change-Id: I4ca1a3771f3ad7298c733fd7c42dc55ffc3cbd1f
diff --git a/src/test/java/com/android/tools/r8/desugar/DesugarLambdaWithAnonymousClass.java b/src/test/java/com/android/tools/r8/desugar/DesugarLambdaWithAnonymousClass.java
new file mode 100644
index 0000000..7192449
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/DesugarLambdaWithAnonymousClass.java
@@ -0,0 +1,251 @@
+// Copyright (c) 2020, 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.desugar;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.D8TestRunResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class DesugarLambdaWithAnonymousClass extends TestBase {
+
+ @Parameterized.Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+ }
+
+ private final TestParameters parameters;
+
+ public DesugarLambdaWithAnonymousClass(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ static class Counter {
+ private int count = 0;
+
+ void increment() {
+ count++;
+ }
+
+ int getCount() {
+ return count;
+ }
+ }
+
+ private void checkEnclosingMethod(CodeInspector inspector) {
+ Counter counter = new Counter();
+ inspector.forAllClasses(
+ clazz -> {
+ if (clazz.getFinalName().endsWith("$TestClass$1")
+ || clazz.getFinalName().endsWith("$TestClass$2")) {
+ counter.increment();
+ assertTrue(clazz.isAnonymousClass());
+ DexMethod enclosingMethod = clazz.getFinalEnclosingMethod();
+ ClassSubject testClassSubject =
+ inspector.clazz(DesugarLambdaWithAnonymousClass.TestClass.class);
+ assertEquals(
+ testClassSubject, inspector.clazz(enclosingMethod.holder.toSourceString()));
+ assertThat(
+ testClassSubject.uniqueMethodWithName(enclosingMethod.name.toString()),
+ isPresent());
+ }
+ });
+ assertEquals(2, counter.getCount());
+ }
+
+ // TODO(158752316): There should be no use of this check.
+ private void checkEnclosingMethodWrong(CodeInspector inspector) {
+ Counter counter = new Counter();
+ inspector.forAllClasses(
+ clazz -> {
+ if (clazz.getFinalName().endsWith("$TestClass$1")
+ || clazz.getFinalName().endsWith("$TestClass$2")) {
+ counter.increment();
+ assertTrue(clazz.isAnonymousClass());
+ DexMethod enclosingMethod = clazz.getFinalEnclosingMethod();
+ ClassSubject testClassSubject =
+ inspector.clazz(DesugarLambdaWithAnonymousClass.TestClass.class);
+ assertEquals(
+ testClassSubject, inspector.clazz(enclosingMethod.holder.toSourceString()));
+ if (enclosingMethod.name.toString().contains("Static")) {
+ assertThat(
+ testClassSubject.uniqueMethodWithName(enclosingMethod.name.toString()),
+ isPresent());
+ } else {
+ assertThat(
+ testClassSubject.uniqueMethodWithName(enclosingMethod.name.toString()),
+ not(isPresent()));
+ }
+ }
+ });
+ assertEquals(2, counter.getCount());
+ }
+
+ private void checkArtResult(D8TestRunResult result) {
+ // TODO(158752316): This should neither return null nor fail.
+ if (parameters.getRuntime().asDex().getVm().getVersion().isOlderThanOrEqual(Version.V4_4_4)
+ || parameters.getRuntime().asDex().getVm().getVersion().isNewerThan(Version.V6_0_1)) {
+ result.assertSuccessWithOutputLines(
+ "Hello from inside <null>", "Hello from inside lambda$testStatic$1");
+ } else {
+ result.assertFailureWithErrorThatThrows(IncompatibleClassChangeError.class);
+ }
+ }
+
+ @BeforeClass
+ public static void checkExpectedJavacNames() throws Exception {
+ CodeInspector inspector =
+ new CodeInspector(
+ ToolHelper.getClassFilesForInnerClasses(DesugarLambdaWithLocalClass.class));
+ String outer = DesugarLambdaWithLocalClass.class.getTypeName();
+ ClassSubject testClass = inspector.clazz(outer + "$TestClass");
+ assertThat(testClass, isPresent());
+ assertThat(testClass.uniqueMethodWithName("lambda$test$0"), isPresent());
+ assertThat(testClass.uniqueMethodWithName("lambda$testStatic$1"), isPresent());
+ assertThat(inspector.clazz(outer + "$TestClass$1MyConsumerImpl"), isPresent());
+ assertThat(inspector.clazz(outer + "$TestClass$2MyConsumerImpl"), isPresent());
+ }
+
+ @Test
+ public void testDefault() throws Exception {
+ if (parameters.getRuntime().isCf()) {
+ // Run on the JVM.
+ testForJvm()
+ .addInnerClasses(DesugarLambdaWithAnonymousClass.class)
+ .run(parameters.getRuntime(), TestClass.class)
+ .inspect(this::checkEnclosingMethod)
+ .assertSuccessWithOutputLines(
+ "Hello from inside lambda$test$0", "Hello from inside lambda$testStatic$1");
+ } else {
+ assert parameters.getRuntime().isDex();
+ // Run on Art.
+ checkArtResult(
+ testForD8()
+ .addInnerClasses(DesugarLambdaWithAnonymousClass.class)
+ .setMinApi(parameters.getApiLevel())
+ .compile()
+ .inspect(this::checkEnclosingMethodWrong)
+ .run(parameters.getRuntime(), TestClass.class));
+ }
+ }
+
+ @Test
+ public void testCfToCf() throws Exception {
+ // Use D8 to desugar with Java classfile output.
+ Path jar =
+ testForD8(Backend.CF)
+ .addInnerClasses(DesugarLambdaWithAnonymousClass.class)
+ .setMinApi(parameters.getApiLevel())
+ .compile()
+ .inspect(this::checkEnclosingMethodWrong)
+ .writeToZip();
+
+ if (parameters.getRuntime().isCf()) {
+ // Run on the JVM.
+ testForJvm()
+ .addProgramFiles(jar)
+ .run(parameters.getRuntime(), TestClass.class)
+ // TODO(158752316): This should not fail.
+ .assertFailureWithErrorThatThrows(InternalError.class);
+ } else {
+ assert parameters.getRuntime().isDex();
+ // Compile to DEX without desugaring and run on Art.
+ checkArtResult(
+ testForD8()
+ .addProgramFiles(jar)
+ .setMinApi(parameters.getApiLevel())
+ .disableDesugaring()
+ .compile()
+ .inspect(this::checkEnclosingMethodWrong)
+ .run(parameters.getRuntime(), TestClass.class));
+ }
+ }
+
+ public interface MyConsumer<T> {
+ void accept(T s);
+ }
+
+ public static class StringList extends ArrayList<String> {
+ public void forEachString(MyConsumer<String> consumer) {
+ for (String s : this) {
+ consumer.accept(s);
+ }
+ }
+ }
+
+ public static class TestClass {
+
+ public void test() {
+ StringList list = new StringList();
+
+ list.add("Hello ");
+ list.add("from ");
+ list.add("inside ");
+
+ list.forEachString(
+ s -> {
+ new MyConsumer<String>() {
+ public void accept(String s) {
+ System.out.print(s);
+ if (s.startsWith("inside")) {
+ if (getClass().getEnclosingMethod() == null) {
+ System.out.println("<null>");
+ } else {
+ System.out.println(getClass().getEnclosingMethod().getName());
+ }
+ }
+ }
+ }.accept(s);
+ });
+ }
+
+ public static void testStatic() {
+ StringList list = new StringList();
+
+ list.add("Hello ");
+ list.add("from ");
+ list.add("inside ");
+
+ list.forEachString(
+ s -> {
+ new MyConsumer<String>() {
+ public void accept(String s) {
+ System.out.print(s);
+ if (s.startsWith("inside")) {
+ if (getClass().getEnclosingMethod() == null) {
+ System.out.println("<null>");
+ } else {
+ System.out.println(getClass().getEnclosingMethod().getName());
+ }
+ }
+ }
+ }.accept(s);
+ });
+ }
+
+ public static void main(String[] args) {
+ new TestClass().test();
+ TestClass.testStatic();
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/DesugarLambdaWithLocalClass.java b/src/test/java/com/android/tools/r8/desugar/DesugarLambdaWithLocalClass.java
new file mode 100644
index 0000000..68fd89e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/DesugarLambdaWithLocalClass.java
@@ -0,0 +1,250 @@
+// Copyright (c) 2020, 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.desugar;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.D8TestRunResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class DesugarLambdaWithLocalClass extends TestBase {
+
+ @Parameterized.Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+ }
+
+ private final TestParameters parameters;
+
+ public DesugarLambdaWithLocalClass(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ static class Counter {
+ private int count = 0;
+
+ void increment() {
+ count++;
+ }
+
+ int getCount() {
+ return count;
+ }
+ }
+
+ private void checkEnclosingMethod(CodeInspector inspector) {
+ Counter counter = new Counter();
+ inspector.forAllClasses(
+ clazz -> {
+ if (clazz.getFinalName().endsWith("MyConsumerImpl")) {
+ counter.increment();
+ assertTrue(clazz.isLocalClass());
+ DexMethod enclosingMethod = clazz.getFinalEnclosingMethod();
+ ClassSubject testClassSubject = inspector.clazz(TestClass.class);
+ assertEquals(
+ testClassSubject, inspector.clazz(enclosingMethod.holder.toSourceString()));
+ assertThat(
+ testClassSubject.uniqueMethodWithName(enclosingMethod.name.toString()),
+ isPresent());
+ }
+ });
+ assertEquals(2, counter.getCount());
+ }
+
+ // TODO(158752316): There should be no use of this check.
+ private void checkEnclosingMethodWrong(CodeInspector inspector) {
+ Counter counter = new Counter();
+ inspector.forAllClasses(
+ clazz -> {
+ if (clazz.getFinalName().endsWith("$TestClass$1MyConsumerImpl")
+ || clazz.getFinalName().endsWith("$TestClass$2MyConsumerImpl")) {
+ counter.increment();
+ assertTrue(clazz.isLocalClass());
+ DexMethod enclosingMethod = clazz.getFinalEnclosingMethod();
+ ClassSubject testClassSubject = inspector.clazz(TestClass.class);
+ assertEquals(
+ testClassSubject, inspector.clazz(enclosingMethod.holder.toSourceString()));
+ if (enclosingMethod.name.toString().contains("Static")) {
+ assertThat(
+ testClassSubject.uniqueMethodWithName(enclosingMethod.name.toString()),
+ isPresent());
+ } else {
+ assertThat(
+ testClassSubject.uniqueMethodWithName(enclosingMethod.name.toString()),
+ not(isPresent()));
+ }
+ }
+ });
+ assertEquals(2, counter.getCount());
+ }
+
+ private void checkArtResult(D8TestRunResult result) {
+ // TODO(158752316): This should neither return null nor fail.
+ if (parameters.getRuntime().asDex().getVm().getVersion().isOlderThanOrEqual(Version.V4_4_4)
+ || parameters.getRuntime().asDex().getVm().getVersion().isNewerThan(Version.V6_0_1)) {
+ result.assertSuccessWithOutputLines(
+ "Hello from inside <null>", "Hello from inside lambda$testStatic$1");
+ } else {
+ result.assertFailureWithErrorThatThrows(IncompatibleClassChangeError.class);
+ }
+ }
+
+ @BeforeClass
+ public static void checkExpectedJavacNames() throws Exception {
+ CodeInspector inspector =
+ new CodeInspector(
+ ToolHelper.getClassFilesForInnerClasses(DesugarLambdaWithAnonymousClass.class));
+ String outer = DesugarLambdaWithAnonymousClass.class.getTypeName();
+ ClassSubject testClass = inspector.clazz(outer + "$TestClass");
+ assertThat(testClass, isPresent());
+ assertThat(testClass.uniqueMethodWithName("lambda$test$0"), isPresent());
+ assertThat(testClass.uniqueMethodWithName("lambda$testStatic$1"), isPresent());
+ assertThat(inspector.clazz(outer + "$TestClass$1"), isPresent());
+ assertThat(inspector.clazz(outer + "$TestClass$2"), isPresent());
+ }
+
+ @Test
+ public void testDefault() throws Exception {
+ if (parameters.getRuntime().isCf()) {
+ // Run on the JVM.
+ testForJvm()
+ .addInnerClasses(DesugarLambdaWithLocalClass.class)
+ .run(parameters.getRuntime(), TestClass.class)
+ .inspect(this::checkEnclosingMethod)
+ .assertSuccessWithOutputLines(
+ "Hello from inside lambda$test$0", "Hello from inside lambda$testStatic$1");
+ } else {
+ assert parameters.getRuntime().isDex();
+ // Run on Art.
+ checkArtResult(
+ testForD8()
+ .addInnerClasses(DesugarLambdaWithLocalClass.class)
+ .setMinApi(parameters.getApiLevel())
+ .compile()
+ .inspect(this::checkEnclosingMethodWrong)
+ .run(parameters.getRuntime(), TestClass.class));
+ }
+ }
+
+ @Test
+ public void testCfToCf() throws Exception {
+ // Use D8 to desugar with Java classfile output.
+ Path jar =
+ testForD8(Backend.CF)
+ .addInnerClasses(DesugarLambdaWithLocalClass.class)
+ .setMinApi(parameters.getApiLevel())
+ .compile()
+ .inspect(this::checkEnclosingMethodWrong)
+ .writeToZip();
+
+ if (parameters.getRuntime().isCf()) {
+ // Run on the JVM.
+ testForJvm()
+ .addProgramFiles(jar)
+ .run(parameters.getRuntime(), TestClass.class)
+ // TODO(158752316): This should not fail.
+ .assertFailureWithErrorThatThrows(InternalError.class);
+ } else {
+ assert parameters.getRuntime().isDex();
+ // Compile to DEX without desugaring and run on Art.
+ checkArtResult(
+ testForD8()
+ .addProgramFiles(jar)
+ .setMinApi(parameters.getApiLevel())
+ .disableDesugaring()
+ .compile()
+ .inspect(this::checkEnclosingMethodWrong)
+ .run(parameters.getRuntime(), TestClass.class));
+ }
+ }
+
+ public interface MyConsumer<T> {
+ void accept(T s);
+ }
+
+ public static class StringList extends ArrayList<String> {
+ public void forEachString(MyConsumer<String> consumer) {
+ for (String s : this) {
+ consumer.accept(s);
+ }
+ }
+ }
+
+ public static class TestClass {
+
+ public void test() {
+ StringList list = new StringList();
+
+ list.add("Hello ");
+ list.add("from ");
+ list.add("inside ");
+
+ list.forEachString(
+ s -> {
+ class MyConsumerImpl implements MyConsumer<String> {
+ public void accept(String s) {
+ System.out.print(s);
+ if (s.startsWith("inside")) {
+ if (getClass().getEnclosingMethod() == null) {
+ System.out.println("<null>");
+ } else {
+ System.out.println(getClass().getEnclosingMethod().getName());
+ }
+ }
+ }
+ }
+ new MyConsumerImpl().accept(s);
+ });
+ }
+
+ public static void testStatic() {
+ StringList list = new StringList();
+
+ list.add("Hello ");
+ list.add("from ");
+ list.add("inside ");
+
+ list.forEachString(
+ s -> {
+ class MyConsumerImpl implements MyConsumer<String> {
+ public void accept(String s) {
+ System.out.print(s);
+ if (s.startsWith("inside")) {
+ if (getClass().getEnclosingMethod() == null) {
+ System.out.println("<null>");
+ } else {
+ System.out.println(getClass().getEnclosingMethod().getName());
+ }
+ }
+ }
+ }
+ new MyConsumerImpl().accept(s);
+ });
+ }
+
+ public static void main(String[] args) {
+ new TestClass().test();
+ TestClass.testStatic();
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentClassSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentClassSubject.java
index 47a6182..1491ce0 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentClassSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentClassSubject.java
@@ -5,6 +5,7 @@
package com.android.tools.r8.utils.codeinspector;
import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexProgramClass;
import java.util.List;
import java.util.function.Consumer;
@@ -138,6 +139,11 @@
}
@Override
+ public DexMethod getFinalEnclosingMethod() {
+ throw new Unreachable("Cannot determine EnclosingMethod attribute of an absent class");
+ }
+
+ @Override
public String getOriginalSignatureAttribute() {
return null;
}
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/ClassSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/ClassSubject.java
index 8fed691..bede2a0 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/ClassSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/ClassSubject.java
@@ -4,6 +4,7 @@
package com.android.tools.r8.utils.codeinspector;
+import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.naming.MemberNaming.MethodSignature;
import com.android.tools.r8.references.MethodReference;
@@ -178,6 +179,8 @@
public abstract boolean isSynthesizedJavaLambdaClass();
+ public abstract DexMethod getFinalEnclosingMethod();
+
public abstract String getOriginalSignatureAttribute();
public abstract String getFinalSignatureAttribute();
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java
index b2edae2..2a4c7c5 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java
@@ -83,12 +83,12 @@
this(Collections.singletonList(file), null, null);
}
- public CodeInspector(List<Path> files) throws IOException {
+ public CodeInspector(Collection<Path> files) throws IOException {
this(files, null, null);
}
public CodeInspector(
- List<Path> files, String mappingFile, Consumer<InternalOptions> optionsConsumer)
+ Collection<Path> files, String mappingFile, Consumer<InternalOptions> optionsConsumer)
throws IOException {
Path mappingPath = mappingFile != null ? Paths.get(mappingFile) : null;
if (mappingPath != null && Files.exists(mappingPath)) {
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundClassSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundClassSubject.java
index 6923021..6774c2a 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundClassSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundClassSubject.java
@@ -308,6 +308,11 @@
}
@Override
+ public DexMethod getFinalEnclosingMethod() {
+ return dexClass.getEnclosingMethodAttribute().getEnclosingMethod();
+ }
+
+ @Override
public String getOriginalSignatureAttribute() {
return codeInspector.getOriginalSignatureAttribute(
dexClass.annotations(), GenericSignatureParser::parseClassSignature);