[Retrace] Add support for circular reference exception lines

Bug: 147975151
Change-Id: I9c6c5f9021e8267c9e77195a52ef158557aa652f
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceStackTrace.java b/src/main/java/com/android/tools/r8/retrace/RetraceStackTrace.java
index 3b68d3d..8d3e956 100644
--- a/src/main/java/com/android/tools/r8/retrace/RetraceStackTrace.java
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceStackTrace.java
@@ -458,6 +458,69 @@
     }
   }
 
+  static class CircularReferenceLine extends StackTraceLine {
+
+    private final String startWhitespace;
+    private final String exceptionClass;
+    private final String endBracketAndWhitespace;
+
+    private static final String CIRCULAR_REFERENCE = "[CIRCULAR REFERENCE:";
+
+    public CircularReferenceLine(
+        String startWhitespace, String exceptionClass, String endBracketAndWhitespace) {
+      this.startWhitespace = startWhitespace;
+      this.exceptionClass = exceptionClass;
+      this.endBracketAndWhitespace = endBracketAndWhitespace;
+    }
+
+    static StackTraceLine tryParse(String line) {
+      // Check that the line is indented with some amount of white space.
+      if (line.length() == 0 || !Character.isWhitespace(line.charAt(0))) {
+        return null;
+      }
+      // Find the first non-white space character and check that we have the sequence
+      // '[CIRCULAR REFERENCE:Exception]'.
+      int firstNonWhiteSpace = firstNonWhiteSpaceCharacterFromIndex(line, 0);
+      if (!line.startsWith(CIRCULAR_REFERENCE, firstNonWhiteSpace)) {
+        return null;
+      }
+      int exceptionStartIndex = firstNonWhiteSpace + CIRCULAR_REFERENCE.length();
+      int lastBracketPosition = firstCharFromIndex(line, exceptionStartIndex, ']');
+      if (lastBracketPosition == line.length()) {
+        return null;
+      }
+      int onlyWhitespaceFromLastBracket =
+          firstNonWhiteSpaceCharacterFromIndex(line, lastBracketPosition + 1);
+      if (onlyWhitespaceFromLastBracket != line.length()) {
+        return null;
+      }
+      return new CircularReferenceLine(
+          line.substring(0, firstNonWhiteSpace),
+          line.substring(exceptionStartIndex, lastBracketPosition),
+          line.substring(lastBracketPosition));
+    }
+
+    @Override
+    List<StackTraceLine> retrace(RetraceBase retraceBase, boolean verbose) {
+      List<StackTraceLine> exceptionLines = new ArrayList<>();
+      retraceBase
+          .retrace(Reference.classFromTypeName(exceptionClass))
+          .forEach(
+              element ->
+                  exceptionLines.add(
+                      new CircularReferenceLine(
+                          startWhitespace,
+                          element.getClassReference().getTypeName(),
+                          endBracketAndWhitespace)));
+      return exceptionLines;
+    }
+
+    @Override
+    public String toString() {
+      return startWhitespace + CIRCULAR_REFERENCE + exceptionClass + endBracketAndWhitespace;
+    }
+  }
+
   static class UnknownLine extends StackTraceLine {
     private final String line;
 
@@ -490,6 +553,10 @@
     if (parsedLine != null) {
       return parsedLine;
     }
+    parsedLine = CircularReferenceLine.tryParse(line);
+    if (parsedLine != null) {
+      return parsedLine;
+    }
     parsedLine = MoreLine.tryParse(line);
     if (parsedLine == null) {
       diagnosticsHandler.warning(
diff --git a/src/test/java/com/android/tools/r8/retrace/RetraceTests.java b/src/test/java/com/android/tools/r8/retrace/RetraceTests.java
index e29c928..e50ca91 100644
--- a/src/test/java/com/android/tools/r8/retrace/RetraceTests.java
+++ b/src/test/java/com/android/tools/r8/retrace/RetraceTests.java
@@ -8,6 +8,7 @@
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestDiagnosticMessagesImpl;
@@ -18,6 +19,7 @@
 import com.android.tools.r8.retrace.stacktraces.ActualRetraceBotStackTrace;
 import com.android.tools.r8.retrace.stacktraces.AmbiguousMissingLineStackTrace;
 import com.android.tools.r8.retrace.stacktraces.AmbiguousStackTrace;
+import com.android.tools.r8.retrace.stacktraces.CircularReferenceStackTrace;
 import com.android.tools.r8.retrace.stacktraces.FileNameExtensionStackTrace;
 import com.android.tools.r8.retrace.stacktraces.InlineFileNameStackTrace;
 import com.android.tools.r8.retrace.stacktraces.InlineNoLineNumberStackTrace;
@@ -149,6 +151,14 @@
     runRetraceTest(new InlineNoLineNumberStackTrace());
   }
 
+  @Test
+  public void testCircularReferenceStackTrace() {
+    // Proguard retrace (and therefore the default regular expression) will not retrace circular
+    // reference exceptions.
+    assumeFalse(useRegExpParsing);
+    runRetraceTest(new CircularReferenceStackTrace());
+  }
+
   private TestDiagnosticMessagesImpl runRetraceTest(StackTraceForTest stackTraceForTest) {
     TestDiagnosticMessagesImpl diagnosticsHandler = new TestDiagnosticMessagesImpl();
     RetraceCommand retraceCommand =
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/CircularReferenceStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/CircularReferenceStackTrace.java
new file mode 100644
index 0000000..5df37f8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/CircularReferenceStackTrace.java
@@ -0,0 +1,49 @@
+// 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.retrace.stacktraces;
+
+import com.android.tools.r8.utils.StringUtils;
+import java.util.Arrays;
+import java.util.List;
+
+public class CircularReferenceStackTrace implements StackTraceForTest {
+
+  @Override
+  public List<String> obfuscatedStackTrace() {
+    return Arrays.asList(
+        "        [CIRCULAR REFERENCE:A.A]",
+        " [CIRCULAR REFERENCE:A.B]",
+        "        [CIRCULAR REFERENCE:None.existing.class]",
+        "        [CIRCULAR REFERENCE:A.A] ",
+        // Invalid Circular Reference lines.
+        "        [CIRCU:AA]",
+        "        [CIRCULAR REFERENCE:A.A",
+        "        [CIRCULAR REFERENCE:]",
+        "        [CIRCULAR REFERENCE:None existing class]");
+  }
+
+  @Override
+  public String mapping() {
+    return StringUtils.lines("foo.bar.Baz -> A.A:", "foo.bar.Qux -> A.B:");
+  }
+
+  @Override
+  public List<String> retracedStackTrace() {
+    return Arrays.asList(
+        "        [CIRCULAR REFERENCE:foo.bar.Baz]",
+        " [CIRCULAR REFERENCE:foo.bar.Qux]",
+        "        [CIRCULAR REFERENCE:None.existing.class]",
+        "        [CIRCULAR REFERENCE:foo.bar.Baz] ",
+        "        [CIRCU:AA]",
+        "        [CIRCULAR REFERENCE:A.A",
+        "        [CIRCULAR REFERENCE:]",
+        "        [CIRCULAR REFERENCE:None existing class]");
+  }
+
+  @Override
+  public int expectedWarnings() {
+    return 5;
+  }
+}