[Retrace] Add support for source file macro %S used in remapper

Bug: 159425023
Change-Id: Iec1e56e1d84e43b0cc6b78b7b8ac44339c2ee024
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceRegularExpression.java b/src/main/java/com/android/tools/r8/retrace/RetraceRegularExpression.java
index 4973190..20e12e0 100644
--- a/src/main/java/com/android/tools/r8/retrace/RetraceRegularExpression.java
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceRegularExpression.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.retrace;
 
 import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.references.ClassReference;
 import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.references.Reference;
@@ -33,6 +34,9 @@
 
   private static final int NO_MATCH = -1;
 
+  private final RegularExpressionGroup[] syntheticGroups =
+      new RegularExpressionGroup[] {new SourceFileLineNumberGroup()};
+
   private final RegularExpressionGroup[] groups =
       new RegularExpressionGroup[] {
         new TypeNameGroup(),
@@ -130,19 +134,17 @@
       String regularExpression, List<RegularExpressionGroupHandler> handlers) {
     int currentIndex = 0;
     int captureGroupIndex = 0;
+    regularExpression = registerSyntheticGroups(regularExpression);
     while (currentIndex < regularExpression.length()) {
       RegularExpressionGroup firstGroup = null;
       int firstIndexFromCurrent = regularExpression.length();
       for (RegularExpressionGroup group : groups) {
-        int nextIndexOf = regularExpression.indexOf(group.shortName(), currentIndex);
-        if (nextIndexOf > NO_MATCH && nextIndexOf < firstIndexFromCurrent) {
-          // Check if previous character in the regular expression is not \\ to ensure not
-          // overriding a matching on shortName.
-          if (nextIndexOf > 0 && regularExpression.charAt(nextIndexOf - 1) == '\\') {
-            continue;
-          }
+        int firstIndex =
+            firstIndexOfGroup(
+                currentIndex, firstIndexFromCurrent, regularExpression, group.shortName());
+        if (firstIndex > NO_MATCH) {
           firstGroup = group;
-          firstIndexFromCurrent = nextIndexOf;
+          firstIndexFromCurrent = firstIndex;
         }
       }
       if (firstGroup != null) {
@@ -161,6 +163,49 @@
     return regularExpression;
   }
 
+  private int firstIndexOfGroup(int startIndex, int endIndex, String expression, String shortName) {
+    int nextIndexOf = startIndex;
+    while (nextIndexOf != NO_MATCH) {
+      nextIndexOf = expression.indexOf(shortName, nextIndexOf);
+      if (nextIndexOf > NO_MATCH) {
+        if (nextIndexOf < endIndex && !isEscaped(expression, nextIndexOf)) {
+          return nextIndexOf;
+        }
+        nextIndexOf++;
+      }
+    }
+    return NO_MATCH;
+  }
+
+  private boolean isEscaped(String expression, int index) {
+    boolean escaped = false;
+    while (index > 0 && expression.charAt(--index) == '\\') {
+      escaped = !escaped;
+    }
+    return escaped;
+  }
+
+  private String registerSyntheticGroups(String regularExpression) {
+    boolean modifiedExpression;
+    do {
+      modifiedExpression = false;
+      for (RegularExpressionGroup syntheticGroup : syntheticGroups) {
+        int firstIndex =
+            firstIndexOfGroup(
+                0, regularExpression.length(), regularExpression, syntheticGroup.shortName());
+        if (firstIndex > NO_MATCH) {
+          regularExpression =
+              regularExpression.substring(0, firstIndex)
+                  + syntheticGroup.subExpression()
+                  + regularExpression.substring(firstIndex + syntheticGroup.shortName().length());
+          // Loop as long as we can replace.
+          modifiedExpression = true;
+        }
+      }
+    } while (modifiedExpression);
+    return regularExpression;
+  }
+
   static class RetraceString {
 
     private final Element classContext;
@@ -387,6 +432,10 @@
     abstract String subExpression();
 
     abstract RegularExpressionGroupHandler createHandler(String captureGroup);
+
+    boolean isSynthetic() {
+      return false;
+    }
   }
 
   // TODO(b/145731185): Extend support for identifiers with strings inside back ticks.
@@ -718,6 +767,29 @@
     }
   }
 
+  private class SourceFileLineNumberGroup extends RegularExpressionGroup {
+
+    @Override
+    String shortName() {
+      return "%S";
+    }
+
+    @Override
+    String subExpression() {
+      return "%s(?::%l)?";
+    }
+
+    @Override
+    RegularExpressionGroupHandler createHandler(String captureGroup) {
+      throw new Unreachable("Should never be called");
+    }
+
+    @Override
+    boolean isSynthetic() {
+      return true;
+    }
+  }
+
   private static final String JAVA_TYPE_REGULAR_EXPRESSION =
       "(" + javaIdentifierSegment + "\\.)*" + javaIdentifierSegment + "[\\[\\]]*";
 
diff --git a/src/test/java/com/android/tools/r8/retrace/RetraceRegularExpressionTests.java b/src/test/java/com/android/tools/r8/retrace/RetraceRegularExpressionTests.java
index bb69fc3..d8768a2 100644
--- a/src/test/java/com/android/tools/r8/retrace/RetraceRegularExpressionTests.java
+++ b/src/test/java/com/android/tools/r8/retrace/RetraceRegularExpressionTests.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.retrace;
 
+import static com.android.tools.r8.retrace.Retrace.DEFAULT_REGULAR_EXPRESSION;
 import static junit.framework.TestCase.assertEquals;
 
 import com.android.tools.r8.TestBase;
@@ -11,6 +12,7 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.retrace.stacktraces.InlineFileNameStackTrace;
+import com.android.tools.r8.retrace.stacktraces.RetraceAssertionErrorStackTrace;
 import com.android.tools.r8.retrace.stacktraces.StackTraceForTest;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableList;
@@ -24,9 +26,6 @@
 @RunWith(Parameterized.class)
 public class RetraceRegularExpressionTests extends TestBase {
 
-  private static final String DEFAULT_REGULAR_EXPRESSION =
-      "(?:.*?\\bat\\s+%c\\.%m\\s*\\(%s(?::%l)?\\)\\s*(?:~\\[.*\\])?)|(?:(?:.*?[:\"]\\s+)?%c(?::.*)?)";
-
   @Parameters(name = "{0}")
   public static TestParametersCollection data() {
     return getTestParameters().withNoneRuntime().build();
@@ -731,6 +730,41 @@
         });
   }
 
+  @Test
+  public void testSourceFileLineNumber() {
+    runRetraceTest(
+        DEFAULT_REGULAR_EXPRESSION.replace("%s(?::%l)?", "%S"),
+        new RetraceAssertionErrorStackTrace());
+  }
+
+  @Test
+  public void testEscaping() {
+    runRetraceTest(
+        "\\%c\\\\%c\\\\\\%c.%m\\(\\\\%S\\)\\\\\\%S",
+        new StackTraceForTest() {
+          @Override
+          public List<String> obfuscatedStackTrace() {
+            return ImmutableList.of("%c\\com.android.tools.r8.Foo\\%c.a(\\SourceFile:1)\\%S");
+          }
+
+          @Override
+          public String mapping() {
+            return "com.android.tools.r8.Bar -> com.android.tools.r8.Foo:\n"
+                + "  1:1:void m():13:13 -> a";
+          }
+
+          @Override
+          public List<String> retracedStackTrace() {
+            return ImmutableList.of("%c\\com.android.tools.r8.Bar\\%c.m(\\Bar.java:13)\\%S");
+          }
+
+          @Override
+          public int expectedWarnings() {
+            return 0;
+          }
+        });
+  }
+
   private TestDiagnosticMessagesImpl runRetraceTest(
       String regularExpression, StackTraceForTest stackTraceForTest) {
     TestDiagnosticMessagesImpl diagnosticsHandler = new TestDiagnosticMessagesImpl();