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);