Fix stack overflow

Stack overflow was caused by input with "loops" in the
EnclosingMethod/InnerClasses attributes. A concrete case for the
EnclosingMethod case has been seen in class files generated by
the Kotlin compiler.

Bug: b/366140351
Change-Id: I6ee0f9af52fb578235f5e031762b44e67d140026
diff --git a/src/main/java/com/android/tools/r8/graph/GenericSignatureContextBuilder.java b/src/main/java/com/android/tools/r8/graph/GenericSignatureContextBuilder.java
index 0cc9356..84bd5a1 100644
--- a/src/main/java/com/android/tools/r8/graph/GenericSignatureContextBuilder.java
+++ b/src/main/java/com/android/tools/r8/graph/GenericSignatureContextBuilder.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.graph.GenericSignature.FormalTypeParameter;
 import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
 import com.android.tools.r8.utils.WorkList;
+import com.google.common.collect.Sets;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -53,7 +54,6 @@
   }
 
   public static class TypeParameterContext {
-
     private static final TypeParameterContext EMPTY =
         new TypeParameterContext(Collections.emptyMap(), Collections.emptySet());
 
@@ -207,17 +207,38 @@
   public TypeParameterContext computeTypeParameterContext(
       AppView<?> appView, DexReference reference, Predicate<DexType> wasPruned) {
     assert !wasPruned.test(reference.getContextType()) : "Building context for pruned type";
-    return computeTypeParameterContext(appView, reference, wasPruned, false);
+    return computeTypeParameterContext(appView, reference, wasPruned, false, null);
   }
 
   private TypeParameterContext computeTypeParameterContext(
       AppView<?> appView,
       DexReference reference,
       Predicate<DexType> wasPruned,
-      boolean seenPruned) {
+      boolean seenPruned,
+      Object seen) {
     if (reference == null) {
       return empty();
     }
+
+    // Optimize seen set to be null when empty and a DexReference when it only has one element.
+    if (seen == null) {
+      seen = reference;
+    } else if (seen instanceof DexReference) {
+      if (seen == reference) {
+        return empty();
+      }
+      DexReference tmp = (DexReference) seen;
+      seen = Sets.newIdentityHashSet();
+      ((Set<DexReference>) seen).add(tmp);
+    }
+    assert seen != null;
+    assert seen instanceof DexReference || seen instanceof Set;
+    if (seen instanceof Set) {
+      if (!((Set<DexReference>) seen).add(reference)) {
+        return empty();
+      }
+    }
+
     DexType contextType = reference.getContextType();
     // TODO(b/187035453): We should visit generic signatures in the enqueuer.
     DexClass clazz = appView.appInfo().definitionForWithoutExistenceAssert(contextType);
@@ -242,7 +263,8 @@
                 enclosingReference,
                 wasPruned,
                 prunedHere
-                    || hasPrunedRelationship(appView, enclosingReference, contextType, wasPruned))
+                    || hasPrunedRelationship(appView, enclosingReference, contextType, wasPruned),
+                seen)
             // Add formals for the context
             .combine(formalsInfo, prunedHere);
     if (!reference.isDexMethod()) {
diff --git a/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureSelfReferencingEnclosingConstructorTest.java b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureSelfReferencingEnclosingConstructorTest.java
index d9bbc23..79c68c2 100644
--- a/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureSelfReferencingEnclosingConstructorTest.java
+++ b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureSelfReferencingEnclosingConstructorTest.java
@@ -3,19 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph.genericsignature;
 
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
-import com.android.tools.r8.CompilationFailedException;
 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.references.Reference;
-import com.android.tools.r8.utils.ExceptionDiagnostic;
 import com.android.tools.r8.utils.StringUtils;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -40,6 +35,7 @@
   private static final String CONSTRUCTOR =
       TestClass.class.getTypeName() + "$1A(" + TestClass.class.getTypeName() + ")";
   private static final String EXPECTED_OUTPUT = StringUtils.lines(CONSTRUCTOR);
+  private static final String EXPECTED_OUTPUT_R8_DEX = StringUtils.lines("public " + CONSTRUCTOR);
 
   @Test
   public void testJvm() throws Exception {
@@ -65,24 +61,27 @@
 
   @Test
   public void testR8() throws Exception {
-    // TODO(b/366140351): This should not crash the compiler.
-    assertThrows(
-        CompilationFailedException.class,
-        () ->
-            testForR8(parameters.getBackend())
-                .addProgramClasses(TestClass.class)
-                .addProgramClassFileData(
-                    getProgramClassFileDataWithSelfReferencingEnclosingMethod())
-                .addKeepMainRule(TestClass.class)
-                .setMinApi(parameters)
-                .compileWithExpectedDiagnostics(
-                    diagnostics -> {
-                      assertEquals(1, diagnostics.getErrors().size());
-                      assertTrue(diagnostics.getErrors().get(0) instanceof ExceptionDiagnostic);
-                      assertTrue(
-                          ((ExceptionDiagnostic) diagnostics.getErrors().get(0)).getCause()
-                              instanceof StackOverflowError);
-                    }));
+    testForR8(parameters.getBackend())
+        .addProgramClasses(TestClass.class)
+        .addProgramClassFileData(getProgramClassFileDataWithSelfReferencingEnclosingMethod())
+        .addKeepMainRule(TestClass.class)
+        .addKeepAttributeInnerClassesAndEnclosingMethod()
+        .addKeepRules("-keep class **$1A { <methods>; }")
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(
+            parameters.isDexRuntime() ? EXPECTED_OUTPUT_R8_DEX : EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testR8NoKeep() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(TestClass.class)
+        .addProgramClassFileData(getProgramClassFileDataWithSelfReferencingEnclosingMethod())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("null");
   }
 
   // Change the EnclosingMethod attribute for the local class A to be a constructor of A itself.
diff --git a/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureSelfReferencingEnclosingMethodTest.java b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureSelfReferencingEnclosingMethodTest.java
index b3a2931..92685d4 100644
--- a/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureSelfReferencingEnclosingMethodTest.java
+++ b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureSelfReferencingEnclosingMethodTest.java
@@ -3,19 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph.genericsignature;
 
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
-import com.android.tools.r8.CompilationFailedException;
 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.references.Reference;
-import com.android.tools.r8.utils.ExceptionDiagnostic;
 import com.android.tools.r8.utils.StringUtils;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -63,24 +58,26 @@
 
   @Test
   public void testR8() throws Exception {
-    // TODO(b/366140351): This should not crash the compiler.
-    assertThrows(
-        CompilationFailedException.class,
-        () ->
-            testForR8(parameters.getBackend())
-                .addProgramClasses(TestClass.class)
-                .addProgramClassFileData(
-                    getProgramClassFileDataWithSelfReferencingEnclosingMethod())
-                .addKeepMainRule(TestClass.class)
-                .setMinApi(parameters)
-                .compileWithExpectedDiagnostics(
-                    diagnostics -> {
-                      assertEquals(1, diagnostics.getErrors().size());
-                      assertTrue(diagnostics.getErrors().get(0) instanceof ExceptionDiagnostic);
-                      assertTrue(
-                          ((ExceptionDiagnostic) diagnostics.getErrors().get(0)).getCause()
-                              instanceof StackOverflowError);
-                    }));
+    testForR8(parameters.getBackend())
+        .addProgramClasses(TestClass.class)
+        .addProgramClassFileData(getProgramClassFileDataWithSelfReferencingEnclosingMethod())
+        .addKeepMainRule(TestClass.class)
+        .addKeepAttributeInnerClassesAndEnclosingMethod()
+        .addKeepRules("-keep class **$1A { <methods>; }")
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testR8NoKeep() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(TestClass.class)
+        .addProgramClassFileData(getProgramClassFileDataWithSelfReferencingEnclosingMethod())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("No method m", "null");
   }
 
   // Change the EnclosingMethod attribute for the local class A to be method m of A itself.
@@ -101,7 +98,11 @@
       class A {
         public void m() {
           try {
-            System.out.println(getClass().getMethod("m"));
+            try {
+              System.out.println(getClass().getMethod("m"));
+            } catch (NoSuchMethodException e) {
+              System.out.println("No method m");
+            }
             System.out.println(A.class.getEnclosingMethod());
           } catch (Exception e) {
             System.out.println("Unexpected " + e);
diff --git a/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureSelfReferencingInnerClassesTest.java b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureSelfReferencingInnerClassesTest.java
index f0384ac..446978f 100644
--- a/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureSelfReferencingInnerClassesTest.java
+++ b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureSelfReferencingInnerClassesTest.java
@@ -3,20 +3,16 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph.genericsignature;
 
-import static org.junit.Assert.assertEquals;
+import static org.hamcrest.CoreMatchers.containsString;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
-import com.android.tools.r8.CompilationFailedException;
 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.graph.genericsignature.GenericSignatureSelfReferencingInnerClassesTest.A.B;
 import com.android.tools.r8.references.Reference;
-import com.android.tools.r8.utils.ExceptionDiagnostic;
 import com.android.tools.r8.utils.StringUtils;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -64,23 +60,35 @@
 
   @Test
   public void testR8() throws Exception {
-    // TODO(b/366140351): This should not crash the compiler.
-    assertThrows(
-        CompilationFailedException.class,
-        () ->
-            testForR8(parameters.getBackend())
-                .addProgramClasses(TestClass.class)
-                .addProgramClassFileData(getProgramClassFileDataWithSelfReferencingInnerClass())
-                .addKeepMainRule(TestClass.class)
-                .setMinApi(parameters)
-                .compileWithExpectedDiagnostics(
-                    diagnostics -> {
-                      assertEquals(1, diagnostics.getErrors().size());
-                      assertTrue(diagnostics.getErrors().get(0) instanceof ExceptionDiagnostic);
-                      assertTrue(
-                          ((ExceptionDiagnostic) diagnostics.getErrors().get(0)).getCause()
-                              instanceof StackOverflowError);
-                    }));
+    testForR8(parameters.getBackend())
+        .addProgramClasses(TestClass.class)
+        .addProgramClassFileData(getProgramClassFileDataWithSelfReferencingInnerClass())
+        .addKeepMainRule(TestClass.class)
+        .addKeepAttributeInnerClassesAndEnclosingMethod()
+        .addKeepRules("-keep class **B { <methods>; }")
+        .setMinApi(parameters)
+        .allowDiagnosticInfoMessages()
+        .compile()
+        .assertAllInfoMessagesMatch(containsString("Malformed inner-class attribute"))
+        .run(parameters.getRuntime(), TestClass.class)
+        .applyIf(
+            parameters.isCfRuntime(),
+            r -> r.assertFailureWithErrorThatThrows(ClassFormatError.class),
+            r -> r.assertSuccessWithOutput(EXPECTED_OUTPUT));
+  }
+
+  @Test
+  public void testR8NoKeep() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(TestClass.class)
+        .addProgramClassFileData(getProgramClassFileDataWithSelfReferencingInnerClass())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .allowDiagnosticInfoMessages()
+        .compile()
+        .assertAllInfoMessagesMatch(containsString("Malformed inner-class attribute"))
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("class " + getClass().getPackage().getName() + ".a", "null");
   }
 
   // Change the InnerClasses attribute for the inner class B to be B itself.