Retrace: Introduce regular expression parser with wildcards

This CL will introduce support for adding custom regular expression
parsing to retrace. The implementation is done by replacing wildcards
with capture groups, that will be processed in order from left to
right, possibly enriching the context and pruning the result space.

Bug: 145356709
Change-Id: I6780130a663d2e79ac13f8eb307b7b9545f9dee2
diff --git a/src/main/java/com/android/tools/r8/naming/MemberNaming.java b/src/main/java/com/android/tools/r8/naming/MemberNaming.java
index 95fbcc8..84f726d 100644
--- a/src/main/java/com/android/tools/r8/naming/MemberNaming.java
+++ b/src/main/java/com/android/tools/r8/naming/MemberNaming.java
@@ -124,6 +124,16 @@
       return name.indexOf(JAVA_PACKAGE_SEPARATOR) != -1;
     }
 
+    public String toUnqualifiedName() {
+      assert isQualified();
+      return name.substring(name.lastIndexOf(JAVA_PACKAGE_SEPARATOR) + 1);
+    }
+
+    public String toHolderFromQualified() {
+      assert isQualified();
+      return name.substring(0, name.lastIndexOf(JAVA_PACKAGE_SEPARATOR));
+    }
+
     public boolean isMethodSignature() {
       return false;
     }
@@ -286,16 +296,6 @@
       return new MethodSignature(toUnqualifiedName(), type, parameters);
     }
 
-    public String toUnqualifiedName() {
-      assert isQualified();
-      return name.substring(name.lastIndexOf(JAVA_PACKAGE_SEPARATOR) + 1);
-    }
-
-    public String toHolderFromQualified() {
-      assert isQualified();
-      return name.substring(0, name.lastIndexOf(JAVA_PACKAGE_SEPARATOR));
-    }
-
     public DexMethod toDexMethod(DexItemFactory factory, DexType clazz) {
       DexType[] paramTypes = new DexType[parameters.length];
       for (int i = 0; i < parameters.length; i++) {
diff --git a/src/main/java/com/android/tools/r8/retrace/Retrace.java b/src/main/java/com/android/tools/r8/retrace/Retrace.java
index b485868..6a4c2e3 100644
--- a/src/main/java/com/android/tools/r8/retrace/Retrace.java
+++ b/src/main/java/com/android/tools/r8/retrace/Retrace.java
@@ -11,7 +11,6 @@
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.retrace.RetraceCommand.Builder;
 import com.android.tools.r8.retrace.RetraceCommand.ProguardMapProducer;
-import com.android.tools.r8.retrace.RetraceStackTrace.RetraceResult;
 import com.android.tools.r8.utils.OptionsParsing;
 import com.android.tools.r8.utils.OptionsParsing.ParseContext;
 import com.android.tools.r8.utils.StringDiagnostic;
@@ -118,10 +117,21 @@
       ClassNameMapper classNameMapper =
           ClassNameMapper.mapperFromString(command.proguardMapProducer.get());
       RetraceBase retraceBase = new RetraceBaseImpl(classNameMapper);
-      RetraceResult result =
-          new RetraceStackTrace(retraceBase, command.stackTrace, command.diagnosticsHandler)
-              .retrace();
-      command.retracedStackTraceConsumer.accept(result.toListOfStrings());
+      RetraceCommandLineResult result;
+      if (command.regularExpression != null) {
+        result =
+            new RetraceRegularExpression(
+                    retraceBase,
+                    command.stackTrace,
+                    command.diagnosticsHandler,
+                    command.regularExpression)
+                .retrace();
+      } else {
+        result =
+            new RetraceStackTrace(retraceBase, command.stackTrace, command.diagnosticsHandler)
+                .retrace();
+      }
+      command.retracedStackTraceConsumer.accept(result.getNodes());
     } catch (IOException ex) {
       command.diagnosticsHandler.error(
           new StringDiagnostic("Could not open mapping input stream: " + ex.getMessage()));
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceCommand.java b/src/main/java/com/android/tools/r8/retrace/RetraceCommand.java
index 5385b09..b2692a2 100644
--- a/src/main/java/com/android/tools/r8/retrace/RetraceCommand.java
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceCommand.java
@@ -13,6 +13,7 @@
 public class RetraceCommand {
 
   final boolean isVerbose;
+  final String regularExpression;
   final DiagnosticsHandler diagnosticsHandler;
   final ProguardMapProducer proguardMapProducer;
   final List<String> stackTrace;
@@ -20,11 +21,13 @@
 
   private RetraceCommand(
       boolean isVerbose,
+      String regularExpression,
       DiagnosticsHandler diagnosticsHandler,
       ProguardMapProducer proguardMapProducer,
       List<String> stackTrace,
       Consumer<List<String>> retracedStackTraceConsumer) {
     this.isVerbose = isVerbose;
+    this.regularExpression = regularExpression;
     this.diagnosticsHandler = diagnosticsHandler;
     this.proguardMapProducer = proguardMapProducer;
     this.stackTrace = stackTrace;
@@ -55,6 +58,7 @@
     private boolean isVerbose;
     private DiagnosticsHandler diagnosticsHandler;
     private ProguardMapProducer proguardMapProducer;
+    private String regularExpression;
     private List<String> stackTrace;
     private Consumer<List<String>> retracedStackTraceConsumer;
 
@@ -79,6 +83,17 @@
     }
 
     /**
+     * Set a regular expression for parsing the incoming text. The Regular expression must not use
+     * naming groups and has special wild cards according to proguard retrace.
+     *
+     * @param regularExpression The regular expression to use.
+     */
+    public Builder setRegularExpression(String regularExpression) {
+      this.regularExpression = regularExpression;
+      return this;
+    }
+
+    /**
      * Set the obfuscated stack trace that is to be retraced.
      *
      * @param stackTrace Stack trace having the top entry(the closest stack to the error) as the
@@ -114,6 +129,7 @@
       }
       return new RetraceCommand(
           isVerbose,
+          regularExpression,
           diagnosticsHandler,
           proguardMapProducer,
           stackTrace,
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceCommandLineResult.java b/src/main/java/com/android/tools/r8/retrace/RetraceCommandLineResult.java
new file mode 100644
index 0000000..00f1857
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceCommandLineResult.java
@@ -0,0 +1,20 @@
+// Copyright (c) 2019, 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;
+
+import java.util.List;
+
+public class RetraceCommandLineResult {
+
+  private final List<String> nodes;
+
+  RetraceCommandLineResult(List<String> nodes) {
+    this.nodes = nodes;
+  }
+
+  public List<String> getNodes() {
+    return nodes;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceFieldResult.java b/src/main/java/com/android/tools/r8/retrace/RetraceFieldResult.java
index 69a561a..733eec7 100644
--- a/src/main/java/com/android/tools/r8/retrace/RetraceFieldResult.java
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceFieldResult.java
@@ -7,9 +7,11 @@
 import com.android.tools.r8.Keep;
 import com.android.tools.r8.naming.MemberNaming;
 import com.android.tools.r8.naming.MemberNaming.FieldSignature;
+import com.android.tools.r8.references.ClassReference;
 import com.android.tools.r8.references.FieldReference;
 import com.android.tools.r8.references.FieldReference.UnknownFieldReference;
 import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.DescriptorUtils;
 import java.util.List;
 import java.util.Objects;
 import java.util.function.Consumer;
@@ -61,12 +63,20 @@
               assert memberNaming.isFieldNaming();
               FieldSignature fieldSignature =
                   memberNaming.getOriginalSignature().asFieldSignature();
+              ClassReference holder =
+                  fieldSignature.isQualified()
+                      ? Reference.classFromDescriptor(
+                          DescriptorUtils.javaTypeToDescriptor(
+                              fieldSignature.toHolderFromQualified()))
+                      : classElement.getClassReference();
               return new Element(
                   this,
                   classElement,
                   Reference.field(
-                      classElement.getClassReference(),
-                      fieldSignature.name,
+                      holder,
+                      fieldSignature.isQualified()
+                          ? fieldSignature.toUnqualifiedName()
+                          : fieldSignature.name,
                       Reference.typeFromTypeName(fieldSignature.type)));
             });
   }
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceMethodResult.java b/src/main/java/com/android/tools/r8/retrace/RetraceMethodResult.java
index 77b4761..347c646 100644
--- a/src/main/java/com/android/tools/r8/retrace/RetraceMethodResult.java
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceMethodResult.java
@@ -38,6 +38,10 @@
     assert classElement != null;
   }
 
+  public UnknownMethodReference getUnknownReference() {
+    return new UnknownMethodReference(classElement.getClassReference(), obfuscatedName);
+  }
+
   private boolean hasRetraceResult() {
     return mappedRanges != null && mappedRanges.getMappedRanges().size() > 0;
   }
@@ -88,12 +92,7 @@
 
   Stream<Element> stream() {
     if (!hasRetraceResult()) {
-      return Stream.of(
-          new Element(
-              this,
-              classElement,
-              new UnknownMethodReference(classElement.getClassReference(), obfuscatedName),
-              null));
+      return Stream.of(new Element(this, classElement, getUnknownReference(), null));
     }
     return mappedRanges.getMappedRanges().stream()
         .map(
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceRegularExpression.java b/src/main/java/com/android/tools/r8/retrace/RetraceRegularExpression.java
new file mode 100644
index 0000000..d6b4cb9
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceRegularExpression.java
@@ -0,0 +1,746 @@
+// Copyright (c) 2019, 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;
+
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.references.TypeReference;
+import com.android.tools.r8.retrace.RetraceClassResult.Element;
+import com.android.tools.r8.retrace.RetraceRegularExpression.RetraceString.RetraceStringBuilder;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.StringDiagnostic;
+import com.google.common.collect.Lists;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+public class RetraceRegularExpression {
+
+  private final RetraceBase retraceBase;
+  private final List<String> stackTrace;
+  private final DiagnosticsHandler diagnosticsHandler;
+  private final String regularExpression;
+
+  private static final int NO_MATCH = -1;
+
+  private final RegularExpressionGroup[] groups =
+      new RegularExpressionGroup[] {
+        new TypeNameGroup(),
+        new BinaryNameGroup(),
+        new MethodNameGroup(),
+        new FieldNameGroup(),
+        new SourceFileGroup(),
+        new LineNumberGroup(),
+        new FieldOrReturnTypeGroup(),
+        new MethodArgumentsGroup()
+      };
+
+  private static final String CAPTURE_GROUP_PREFIX = "captureGroup";
+
+  RetraceRegularExpression(
+      RetraceBase retraceBase,
+      List<String> stackTrace,
+      DiagnosticsHandler diagnosticsHandler,
+      String regularExpression) {
+    this.retraceBase = retraceBase;
+    this.stackTrace = stackTrace;
+    this.diagnosticsHandler = diagnosticsHandler;
+    this.regularExpression = regularExpression;
+  }
+
+  public RetraceCommandLineResult retrace() {
+    List<RegularExpressionGroupHandler> handlers = new ArrayList<>();
+    String regularExpression = registerGroups(this.regularExpression, handlers);
+    Pattern compiledPattern = Pattern.compile(regularExpression);
+    List<String> result = new ArrayList<>();
+    for (String string : stackTrace) {
+      Matcher matcher = compiledPattern.matcher(string);
+      List<RetraceString> retracedStrings =
+          Lists.newArrayList(RetraceStringBuilder.create(string).build());
+      if (matcher.matches()) {
+        for (RegularExpressionGroupHandler handler : handlers) {
+          retracedStrings = handler.handleMatch(retracedStrings, matcher, retraceBase);
+        }
+      }
+      if (retracedStrings.isEmpty()) {
+        // We could not find a match. Output the identity.
+        result.add(string);
+      } else {
+        for (RetraceString retracedString : retracedStrings) {
+          result.add(retracedString.getRetracedString());
+        }
+      }
+    }
+    return new RetraceCommandLineResult(result);
+  }
+
+  private String registerGroups(
+      String regularExpression, List<RegularExpressionGroupHandler> handlers) {
+    int currentIndex = 0;
+    int captureGroupIndex = 0;
+    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;
+          }
+          firstGroup = group;
+          firstIndexFromCurrent = nextIndexOf;
+        }
+      }
+      if (firstGroup != null) {
+        String captureGroupName = CAPTURE_GROUP_PREFIX + (captureGroupIndex++);
+        String patternToInsert = "(?<" + captureGroupName + ">" + firstGroup.subExpression() + ")";
+        regularExpression =
+            regularExpression.substring(0, firstIndexFromCurrent)
+                + patternToInsert
+                + regularExpression.substring(
+                    firstIndexFromCurrent + firstGroup.shortName().length());
+        handlers.add(firstGroup.createHandler(captureGroupName));
+        firstIndexFromCurrent += patternToInsert.length();
+      }
+      currentIndex = firstIndexFromCurrent;
+    }
+    return regularExpression;
+  }
+
+  static class RetraceString {
+
+    private final Element classContext;
+    private final ClassNameGroup classNameGroup;
+    private final ClassReference qualifiedContext;
+    private final RetraceMethodResult.Element methodContext;
+    private final TypeReference typeOrReturnTypeContext;
+    private final boolean hasTypeOrReturnTypeContext;
+    private final String retracedString;
+    private final int adjustedIndex;
+
+    private RetraceString(
+        Element classContext,
+        ClassNameGroup classNameGroup,
+        ClassReference qualifiedContext,
+        RetraceMethodResult.Element methodContext,
+        TypeReference typeOrReturnTypeContext,
+        boolean hasTypeOrReturnTypeContext,
+        String retracedString,
+        int adjustedIndex) {
+      this.classContext = classContext;
+      this.classNameGroup = classNameGroup;
+      this.qualifiedContext = qualifiedContext;
+      this.methodContext = methodContext;
+      this.typeOrReturnTypeContext = typeOrReturnTypeContext;
+      this.hasTypeOrReturnTypeContext = hasTypeOrReturnTypeContext;
+      this.retracedString = retracedString;
+      this.adjustedIndex = adjustedIndex;
+    }
+
+    String getRetracedString() {
+      return retracedString;
+    }
+
+    boolean hasTypeOrReturnTypeContext() {
+      return hasTypeOrReturnTypeContext;
+    }
+
+    Element getClassContext() {
+      return classContext;
+    }
+
+    RetraceMethodResult.Element getMethodContext() {
+      return methodContext;
+    }
+
+    TypeReference getTypeOrReturnTypeContext() {
+      return typeOrReturnTypeContext;
+    }
+
+    public ClassReference getQualifiedContext() {
+      return qualifiedContext;
+    }
+
+    RetraceStringBuilder transform() {
+      return RetraceStringBuilder.create(this);
+    }
+
+    static class RetraceStringBuilder {
+
+      private Element classContext;
+      private ClassNameGroup classNameGroup;
+      private ClassReference qualifiedContext;
+      private RetraceMethodResult.Element methodContext;
+      private TypeReference typeOrReturnTypeContext;
+      private boolean hasTypeOrReturnTypeContext;
+      private String retracedString;
+      private int adjustedIndex;
+
+      private int maxReplaceStringIndex = NO_MATCH;
+
+      private RetraceStringBuilder(
+          Element classContext,
+          ClassNameGroup classNameGroup,
+          ClassReference qualifiedContext,
+          RetraceMethodResult.Element methodContext,
+          TypeReference typeOrReturnTypeContext,
+          boolean hasTypeOrReturnTypeContext,
+          String retracedString,
+          int adjustedIndex) {
+        this.classContext = classContext;
+        this.classNameGroup = classNameGroup;
+        this.qualifiedContext = qualifiedContext;
+        this.methodContext = methodContext;
+        this.typeOrReturnTypeContext = typeOrReturnTypeContext;
+        this.hasTypeOrReturnTypeContext = hasTypeOrReturnTypeContext;
+        this.retracedString = retracedString;
+        this.adjustedIndex = adjustedIndex;
+      }
+
+      static RetraceStringBuilder create(String string) {
+        return new RetraceStringBuilder(null, null, null, null, null, false, string, 0);
+      }
+
+      static RetraceStringBuilder create(RetraceString string) {
+        return new RetraceStringBuilder(
+            string.classContext,
+            string.classNameGroup,
+            string.qualifiedContext,
+            string.methodContext,
+            string.typeOrReturnTypeContext,
+            string.hasTypeOrReturnTypeContext,
+            string.retracedString,
+            string.adjustedIndex);
+      }
+
+      RetraceStringBuilder setClassContext(Element classContext, ClassNameGroup classNameGroup) {
+        this.classContext = classContext;
+        this.classNameGroup = classNameGroup;
+        return this;
+      }
+
+      RetraceStringBuilder setMethodContext(RetraceMethodResult.Element methodContext) {
+        this.methodContext = methodContext;
+        return this;
+      }
+
+      RetraceStringBuilder setTypeOrReturnTypeContext(TypeReference typeOrReturnTypeContext) {
+        hasTypeOrReturnTypeContext = true;
+        this.typeOrReturnTypeContext = typeOrReturnTypeContext;
+        return this;
+      }
+
+      RetraceStringBuilder setQualifiedContext(ClassReference qualifiedContext) {
+        this.qualifiedContext = qualifiedContext;
+        return this;
+      }
+
+      RetraceStringBuilder replaceInString(String oldString, String newString) {
+        int oldStringStartIndex = retracedString.indexOf(oldString);
+        assert oldStringStartIndex > NO_MATCH;
+        int oldStringEndIndex = oldStringStartIndex + oldString.length();
+        return replaceInStringRaw(newString, oldStringStartIndex, oldStringEndIndex);
+      }
+
+      RetraceStringBuilder replaceInString(String newString, int originalFrom, int originalTo) {
+        return replaceInStringRaw(
+            newString, originalFrom + adjustedIndex, originalTo + adjustedIndex);
+      }
+
+      RetraceStringBuilder replaceInStringRaw(String newString, int from, int to) {
+        assert from <= to;
+        assert from > maxReplaceStringIndex;
+        String prefix = retracedString.substring(0, from);
+        String postFix = retracedString.substring(to);
+        this.retracedString = prefix + newString + postFix;
+        this.adjustedIndex = adjustedIndex + newString.length() - (to - from);
+        maxReplaceStringIndex = prefix.length() + newString.length();
+        return this;
+      }
+
+      RetraceString build() {
+        return new RetraceString(
+            classContext,
+            classNameGroup,
+            qualifiedContext,
+            methodContext,
+            typeOrReturnTypeContext,
+            hasTypeOrReturnTypeContext,
+            retracedString,
+            adjustedIndex);
+      }
+    }
+  }
+
+  private interface RegularExpressionGroupHandler {
+
+    List<RetraceString> handleMatch(
+        List<RetraceString> strings, Matcher matcher, RetraceBase retraceBase);
+  }
+
+  private abstract static class RegularExpressionGroup {
+
+    abstract String shortName();
+
+    abstract String subExpression();
+
+    abstract RegularExpressionGroupHandler createHandler(String captureGroup);
+  }
+
+  // TODO(b/145731185): Extend support for identifiers with strings inside back ticks.
+  private static final String javaIdentifierSegment = "[\\p{L}\\p{N}_\\p{Sc}]+";
+
+  private abstract static class ClassNameGroup extends RegularExpressionGroup {
+
+    abstract String getClassName(ClassReference classReference);
+
+    abstract ClassReference classFromMatch(String match);
+
+    @Override
+    RegularExpressionGroupHandler createHandler(String captureGroup) {
+      return (strings, matcher, retraceBase) -> {
+        if (matcher.start(captureGroup) == NO_MATCH) {
+          return strings;
+        }
+        String typeName = matcher.group(captureGroup);
+        RetraceClassResult retraceResult = retraceBase.retrace(classFromMatch(typeName));
+        List<RetraceString> retracedStrings = new ArrayList<>();
+        for (RetraceString retraceString : strings) {
+          retraceResult.forEach(
+              element -> {
+                retracedStrings.add(
+                    retraceString
+                        .transform()
+                        .setClassContext(element, this)
+                        .setMethodContext(null)
+                        .replaceInString(
+                            getClassName(element.getClassReference()),
+                            matcher.start(captureGroup),
+                            matcher.end(captureGroup))
+                        .build());
+              });
+        }
+        return retracedStrings;
+      };
+    }
+  }
+
+  private static class TypeNameGroup extends ClassNameGroup {
+
+    @Override
+    String shortName() {
+      return "%c";
+    }
+
+    @Override
+    String subExpression() {
+      return "(" + javaIdentifierSegment + "\\.)*" + javaIdentifierSegment;
+    }
+
+    @Override
+    String getClassName(ClassReference classReference) {
+      return classReference.getTypeName();
+    }
+
+    @Override
+    ClassReference classFromMatch(String match) {
+      return Reference.classFromTypeName(match);
+    }
+  }
+
+  private static class BinaryNameGroup extends ClassNameGroup {
+
+    @Override
+    String shortName() {
+      return "%C";
+    }
+
+    @Override
+    String subExpression() {
+      return "(?:" + javaIdentifierSegment + "\\/)*" + javaIdentifierSegment;
+    }
+
+    @Override
+    String getClassName(ClassReference classReference) {
+      return classReference.getBinaryName();
+    }
+
+    @Override
+    ClassReference classFromMatch(String match) {
+      return Reference.classFromBinaryName(match);
+    }
+  }
+
+  private static class MethodNameGroup extends RegularExpressionGroup {
+
+    @Override
+    String shortName() {
+      return "%m";
+    }
+
+    @Override
+    String subExpression() {
+      return javaIdentifierSegment;
+    }
+
+    @Override
+    RegularExpressionGroupHandler createHandler(String captureGroup) {
+      return (strings, matcher, retraceBase) -> {
+        if (matcher.start(captureGroup) == NO_MATCH) {
+          return strings;
+        }
+        String methodName = matcher.group(captureGroup);
+        List<RetraceString> retracedStrings = new ArrayList<>();
+        for (RetraceString retraceString : strings) {
+          if (retraceString.classContext == null) {
+            retracedStrings.add(retraceString);
+            continue;
+          }
+          retraceString
+              .getClassContext()
+              .lookupMethod(methodName)
+              .forEach(
+                  element -> {
+                    MethodReference methodReference = element.getMethodReference();
+                    if (retraceString.hasTypeOrReturnTypeContext()) {
+                      if (methodReference.getReturnType() == null
+                          && retraceString.getTypeOrReturnTypeContext() != null) {
+                        return;
+                      } else if (methodReference.getReturnType() != null
+                          && !methodReference
+                              .getReturnType()
+                              .equals(retraceString.getTypeOrReturnTypeContext())) {
+                        return;
+                      }
+                    }
+                    RetraceStringBuilder newRetraceString = retraceString.transform();
+                    ClassReference existingClass =
+                        retraceString.getClassContext().getClassReference();
+                    ClassReference holder = methodReference.getHolderClass();
+                    if (holder != existingClass) {
+                      // The element is defined on another holder.
+                      newRetraceString
+                          .replaceInString(
+                              newRetraceString.classNameGroup.getClassName(existingClass),
+                              newRetraceString.classNameGroup.getClassName(holder))
+                          .setQualifiedContext(holder);
+                    }
+                    newRetraceString
+                        .setMethodContext(element)
+                        .replaceInString(
+                            methodReference.getMethodName(),
+                            matcher.start(captureGroup),
+                            matcher.end(captureGroup));
+                    retracedStrings.add(newRetraceString.build());
+                  });
+        }
+        return retracedStrings;
+      };
+    }
+  }
+
+  private static class FieldNameGroup extends RegularExpressionGroup {
+
+    @Override
+    String shortName() {
+      return "%f";
+    }
+
+    @Override
+    String subExpression() {
+      return javaIdentifierSegment;
+    }
+
+    @Override
+    RegularExpressionGroupHandler createHandler(String captureGroup) {
+      return (strings, matcher, retraceBase) -> {
+        if (matcher.start(captureGroup) == NO_MATCH) {
+          return strings;
+        }
+        String methodName = matcher.group(captureGroup);
+        List<RetraceString> retracedStrings = new ArrayList<>();
+        for (RetraceString retraceString : strings) {
+          if (retraceString.getClassContext() == null) {
+            retracedStrings.add(retraceString);
+            continue;
+          }
+          retraceString
+              .getClassContext()
+              .lookupField(methodName)
+              .forEach(
+                  element -> {
+                    RetraceStringBuilder newRetraceString = retraceString.transform();
+                    ClassReference existingClass =
+                        retraceString.getClassContext().getClassReference();
+                    ClassReference holder = element.getFieldReference().getHolderClass();
+                    if (holder != existingClass) {
+                      // The element is defined on another holder.
+                      newRetraceString
+                          .replaceInString(
+                              newRetraceString.classNameGroup.getClassName(existingClass),
+                              newRetraceString.classNameGroup.getClassName(holder))
+                          .setQualifiedContext(holder);
+                    }
+                    newRetraceString.replaceInString(
+                        element.getFieldReference().getFieldName(),
+                        matcher.start(captureGroup),
+                        matcher.end(captureGroup));
+                    retracedStrings.add(newRetraceString.build());
+                  });
+        }
+        return retracedStrings;
+      };
+    }
+  }
+
+  private static class SourceFileGroup extends RegularExpressionGroup {
+
+    @Override
+    String shortName() {
+      return "%s";
+    }
+
+    @Override
+    String subExpression() {
+      return "(?:\\w+\\.)*\\w+";
+    }
+
+    @Override
+    RegularExpressionGroupHandler createHandler(String captureGroup) {
+      return (strings, matcher, retraceBase) -> {
+        if (matcher.start(captureGroup) == NO_MATCH) {
+          return strings;
+        }
+        String fileName = matcher.group(captureGroup);
+        List<RetraceString> retracedStrings = new ArrayList<>();
+        for (RetraceString retraceString : strings) {
+          if (retraceString.classContext == null) {
+            retracedStrings.add(retraceString);
+            continue;
+          }
+          String newSourceFile =
+              retraceString.getQualifiedContext() != null
+                  ? retraceBase.retraceSourceFile(
+                      retraceString.classContext.getClassReference(),
+                      fileName,
+                      retraceString.getQualifiedContext(),
+                      true)
+                  : retraceString.classContext.retraceSourceFile(fileName, retraceBase);
+          retracedStrings.add(
+              retraceString
+                  .transform()
+                  .replaceInString(
+                      newSourceFile, matcher.start(captureGroup), matcher.end(captureGroup))
+                  .build());
+        }
+        return retracedStrings;
+      };
+    }
+  }
+
+  private class LineNumberGroup extends RegularExpressionGroup {
+
+    @Override
+    String shortName() {
+      return "%l";
+    }
+
+    @Override
+    String subExpression() {
+      return "\\d*";
+    }
+
+    @Override
+    RegularExpressionGroupHandler createHandler(String captureGroup) {
+      return (strings, matcher, retraceBase) -> {
+        if (matcher.start(captureGroup) == NO_MATCH) {
+          return strings;
+        }
+        String lineNumberAsString = matcher.group(captureGroup);
+        int lineNumber =
+            lineNumberAsString.isEmpty() ? NO_MATCH : Integer.parseInt(lineNumberAsString);
+        List<RetraceString> retracedStrings = new ArrayList<>();
+        for (RetraceString retraceString : strings) {
+          RetraceMethodResult.Element methodContext = retraceString.methodContext;
+          if (methodContext == null) {
+            retracedStrings.add(retraceString);
+            continue;
+          }
+          Set<MethodReference> narrowedSet =
+              methodContext.getRetraceMethodResult().narrowByLine(lineNumber).stream()
+                  .map(RetraceMethodResult.Element::getMethodReference)
+                  .collect(Collectors.toSet());
+          if (!narrowedSet.contains(methodContext.getMethodReference())) {
+            // Prune the retraceString since we now have line number information and this is not
+            // a part of the result.
+            diagnosticsHandler.info(
+                new StringDiagnostic(
+                    "Pruning "
+                        + retraceString.getRetracedString()
+                        + " from result because line number "
+                        + lineNumber
+                        + " does not match."));
+            continue;
+          }
+          String newLineNumber =
+              lineNumber > NO_MATCH
+                  ? methodContext.getOriginalLineNumber(lineNumber) + ""
+                  : lineNumberAsString;
+          retracedStrings.add(
+              retraceString
+                  .transform()
+                  .replaceInString(
+                      newLineNumber, matcher.start(captureGroup), matcher.end(captureGroup))
+                  .build());
+        }
+        return retracedStrings;
+      };
+    }
+  }
+
+  private static final String JAVA_TYPE_REGULAR_EXPRESSION =
+      "(" + javaIdentifierSegment + "\\.)*" + javaIdentifierSegment + "[\\[\\]]*";
+
+  private static class FieldOrReturnTypeGroup extends RegularExpressionGroup {
+
+    @Override
+    String shortName() {
+      return "%t";
+    }
+
+    @Override
+    String subExpression() {
+      return JAVA_TYPE_REGULAR_EXPRESSION;
+    }
+
+    @Override
+    RegularExpressionGroupHandler createHandler(String captureGroup) {
+      return (strings, matcher, retraceBase) -> {
+        if (matcher.start(captureGroup) == NO_MATCH) {
+          return strings;
+        }
+        String typeName = matcher.group(captureGroup);
+        String descriptor = DescriptorUtils.javaTypeToDescriptor(typeName);
+        if (!DescriptorUtils.isDescriptor(descriptor) && !"V".equals(descriptor)) {
+          return strings;
+        }
+        TypeReference typeReference = Reference.returnTypeFromDescriptor(descriptor);
+        List<RetraceString> retracedStrings = new ArrayList<>();
+        RetraceTypeResult retracedType = retraceBase.retrace(typeReference);
+        for (RetraceString retraceString : strings) {
+          retracedType.forEach(
+              element -> {
+                TypeReference retracedReference = element.getTypeReference();
+                retracedStrings.add(
+                    retraceString
+                        .transform()
+                        .setTypeOrReturnTypeContext(retracedReference)
+                        .replaceInString(
+                            retracedReference == null ? "void" : retracedReference.getTypeName(),
+                            matcher.start(captureGroup),
+                            matcher.end(captureGroup))
+                        .build());
+              });
+        }
+        return retracedStrings;
+      };
+    }
+  }
+
+  private class MethodArgumentsGroup extends RegularExpressionGroup {
+
+    @Override
+    String shortName() {
+      return "%a";
+    }
+
+    @Override
+    String subExpression() {
+      return "((" + JAVA_TYPE_REGULAR_EXPRESSION + "\\,)*" + JAVA_TYPE_REGULAR_EXPRESSION + ")?";
+    }
+
+    @Override
+    RegularExpressionGroupHandler createHandler(String captureGroup) {
+      return (strings, matcher, retraceBase) -> {
+        if (matcher.start(captureGroup) == NO_MATCH) {
+          return strings;
+        }
+        Set<List<TypeReference>> initialValue = new LinkedHashSet<>();
+        initialValue.add(new ArrayList<>());
+        Set<List<TypeReference>> allRetracedReferences =
+            Arrays.stream(matcher.group(captureGroup).split(","))
+                .map(String::trim)
+                .reduce(
+                    initialValue,
+                    (acc, typeName) -> {
+                      String descriptor = DescriptorUtils.javaTypeToDescriptor(typeName);
+                      if (!DescriptorUtils.isDescriptor(descriptor) && !"V".equals(descriptor)) {
+                        return acc;
+                      }
+                      TypeReference typeReference = Reference.returnTypeFromDescriptor(descriptor);
+                      Set<List<TypeReference>> retracedTypes = new LinkedHashSet<>();
+                      retraceBase
+                          .retrace(typeReference)
+                          .forEach(
+                              element -> {
+                                for (List<TypeReference> currentReferences : acc) {
+                                  ArrayList<TypeReference> newList =
+                                      new ArrayList<>(currentReferences);
+                                  newList.add(element.getTypeReference());
+                                  retracedTypes.add(newList);
+                                }
+                              });
+                      return retracedTypes;
+                    },
+                    (l1, l2) -> {
+                      l1.addAll(l2);
+                      return l1;
+                    });
+        List<RetraceString> retracedStrings = new ArrayList<>();
+        for (RetraceString retraceString : strings) {
+          if (retraceString.getMethodContext() != null
+              && !allRetracedReferences.contains(
+                  retraceString.getMethodContext().getMethodReference().getFormalTypes())) {
+            // Prune the string since we now know the formals.
+            String formals =
+                retraceString.getMethodContext().getMethodReference().getFormalTypes().stream()
+                    .map(TypeReference::getTypeName)
+                    .collect(Collectors.joining(","));
+            diagnosticsHandler.info(
+                new StringDiagnostic(
+                    "Pruning "
+                        + retraceString.getRetracedString()
+                        + " from result because formals ("
+                        + formals
+                        + ") do not match result set."));
+            continue;
+          }
+          for (List<TypeReference> retracedReferences : allRetracedReferences) {
+            retracedStrings.add(
+                retraceString
+                    .transform()
+                    .replaceInString(
+                        retracedReferences.stream()
+                            .map(TypeReference::getTypeName)
+                            .collect(Collectors.joining(",")),
+                        matcher.start(captureGroup),
+                        matcher.end(captureGroup))
+                    .build());
+          }
+        }
+        return retracedStrings;
+      };
+    }
+  }
+}
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 1f4fa43..7c899a7 100644
--- a/src/main/java/com/android/tools/r8/retrace/RetraceStackTrace.java
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceStackTrace.java
@@ -79,23 +79,6 @@
     }
   }
 
-  static class RetraceResult {
-
-    private final List<StackTraceNode> nodes;
-
-    RetraceResult(List<StackTraceNode> nodes) {
-      this.nodes = nodes;
-    }
-
-    List<String> toListOfStrings() {
-      List<String> strings = new ArrayList<>(nodes.size());
-      for (StackTraceNode node : nodes) {
-        node.append(strings);
-      }
-      return strings;
-    }
-  }
-
   private final RetraceBase retraceBase;
   private final List<String> stackTrace;
   private final DiagnosticsHandler diagnosticsHandler;
@@ -107,10 +90,14 @@
     this.diagnosticsHandler = diagnosticsHandler;
   }
 
-  public RetraceResult retrace() {
+  public RetraceCommandLineResult retrace() {
     ArrayList<StackTraceNode> result = new ArrayList<>();
     retraceLine(stackTrace, 0, result);
-    return new RetraceResult(result);
+    List<String> retracedStrings = new ArrayList<>();
+    for (StackTraceNode node : result) {
+      node.append(retracedStrings);
+    }
+    return new RetraceCommandLineResult(retracedStrings);
   }
 
   private void retraceLine(List<String> stackTrace, int index, List<StackTraceNode> result) {
diff --git a/src/test/java/com/android/tools/r8/retrace/RetraceRegularExpressionTests.java b/src/test/java/com/android/tools/r8/retrace/RetraceRegularExpressionTests.java
new file mode 100644
index 0000000..bb69fc3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/retrace/RetraceRegularExpressionTests.java
@@ -0,0 +1,750 @@
+// Copyright (c) 2019, 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;
+
+import static junit.framework.TestCase.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestDiagnosticMessagesImpl;
+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.StackTraceForTest;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@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();
+  }
+
+  public RetraceRegularExpressionTests(TestParameters parameters) {}
+
+  @Test
+  public void ensureNotMatchingOnLiteral() {
+    runRetraceTest(
+        "foo\\%c",
+        new StackTraceForTest() {
+          @Override
+          public List<String> obfuscatedStackTrace() {
+            return ImmutableList.of("foocom.android.tools.r8.a");
+          }
+
+          @Override
+          public String mapping() {
+            return "com.android.tools.r8.R8 -> com.android.tools.r8.a:";
+          }
+
+          @Override
+          public List<String> retracedStackTrace() {
+            return ImmutableList.of("foocom.android.tools.r8.a");
+          }
+
+          @Override
+          public int expectedWarnings() {
+            return 0;
+          }
+        });
+  }
+
+  @Test
+  public void matchMultipleTypeNamesOnLine() {
+    runRetraceTest(
+        "%c\\s%c\\s%c",
+        new StackTraceForTest() {
+          @Override
+          public List<String> obfuscatedStackTrace() {
+            return ImmutableList.of("a.a.a b.b.b c.c.c");
+          }
+
+          @Override
+          public String mapping() {
+            return StringUtils.lines(
+                "AA.AA.AA -> a.a.a:", "BB.BB.BB -> b.b.b:", "CC.CC.CC -> c.c.c:");
+          }
+
+          @Override
+          public List<String> retracedStackTrace() {
+            return ImmutableList.of("AA.AA.AA BB.BB.BB CC.CC.CC");
+          }
+
+          @Override
+          public int expectedWarnings() {
+            return 0;
+          }
+        });
+  }
+
+  @Test
+  public void matchMultipleSlashNamesOnLine() {
+    runRetraceTest(
+        "%C\\s%C\\s%C",
+        new StackTraceForTest() {
+          @Override
+          public List<String> obfuscatedStackTrace() {
+            return ImmutableList.of("a/a/a b/b/b c/c/c");
+          }
+
+          @Override
+          public String mapping() {
+            return StringUtils.lines("AA.AA -> a.a.a:", "BB.BB -> b.b.b:", "CC.CC -> c.c.c:");
+          }
+
+          @Override
+          public List<String> retracedStackTrace() {
+            return ImmutableList.of("AA/AA BB/BB CC/CC");
+          }
+
+          @Override
+          public int expectedWarnings() {
+            return 0;
+          }
+        });
+  }
+
+  @Test
+  public void matchMethodNameInNoContext() {
+    runRetraceTest(
+        "a.b.c.%m",
+        new StackTraceForTest() {
+          @Override
+          public List<String> obfuscatedStackTrace() {
+            return ImmutableList.of("a.b.c.a");
+          }
+
+          @Override
+          public String mapping() {
+            return StringUtils.lines("com.android.tools.r8.R8 -> a.b.c:", "  void foo() -> a");
+          }
+
+          @Override
+          public List<String> retracedStackTrace() {
+            return ImmutableList.of("a.b.c.a");
+          }
+
+          @Override
+          public int expectedWarnings() {
+            return 0;
+          }
+        });
+  }
+
+  @Test
+  public void matchMethodNameInContext() {
+    runRetraceTest(
+        "%c.%m",
+        new StackTraceForTest() {
+          @Override
+          public List<String> obfuscatedStackTrace() {
+            return ImmutableList.of("a.b.c.a");
+          }
+
+          @Override
+          public String mapping() {
+            return StringUtils.lines("com.android.tools.r8.R8 -> a.b.c:", "  void foo() -> a");
+          }
+
+          @Override
+          public List<String> retracedStackTrace() {
+            return ImmutableList.of("com.android.tools.r8.R8.foo");
+          }
+
+          @Override
+          public int expectedWarnings() {
+            return 0;
+          }
+        });
+  }
+
+  @Test
+  public void matchUnknownMethodNameInContext() {
+    runRetraceTest(
+        "%c.%m",
+        new StackTraceForTest() {
+          @Override
+          public List<String> obfuscatedStackTrace() {
+            return ImmutableList.of("a.b.c.a");
+          }
+
+          @Override
+          public String mapping() {
+            return StringUtils.lines("com.android.tools.r8.R8 -> a.b.c:", "  void foo() -> b");
+          }
+
+          @Override
+          public List<String> retracedStackTrace() {
+            return ImmutableList.of("com.android.tools.r8.R8.a");
+          }
+
+          @Override
+          public int expectedWarnings() {
+            return 0;
+          }
+        });
+  }
+
+  @Test
+  public void matchQualifiedMethodInTrace() {
+    runRetraceTest(DEFAULT_REGULAR_EXPRESSION, new InlineFileNameStackTrace());
+  }
+
+  @Test
+  public void matchFieldNameInNoContext() {
+    runRetraceTest(
+        "a.b.c.%f",
+        new StackTraceForTest() {
+          @Override
+          public List<String> obfuscatedStackTrace() {
+            return ImmutableList.of("a.b.c.a");
+          }
+
+          @Override
+          public String mapping() {
+            return StringUtils.lines("com.android.tools.r8.R8 -> a.b.c:", "  int foo -> a");
+          }
+
+          @Override
+          public List<String> retracedStackTrace() {
+            return ImmutableList.of("a.b.c.a");
+          }
+
+          @Override
+          public int expectedWarnings() {
+            return 0;
+          }
+        });
+  }
+
+  @Test
+  public void matchFieldNameInContext() {
+    runRetraceTest(
+        "%c.%f",
+        new StackTraceForTest() {
+          @Override
+          public List<String> obfuscatedStackTrace() {
+            return ImmutableList.of("a.b.c.a");
+          }
+
+          @Override
+          public String mapping() {
+            return StringUtils.lines("com.android.tools.r8.R8 -> a.b.c:", "  int foo -> a");
+          }
+
+          @Override
+          public List<String> retracedStackTrace() {
+            return ImmutableList.of("com.android.tools.r8.R8.foo");
+          }
+
+          @Override
+          public int expectedWarnings() {
+            return 0;
+          }
+        });
+  }
+
+  @Test
+  public void matchUnknownFieldNameInContext() {
+    runRetraceTest(
+        "%c.%f",
+        new StackTraceForTest() {
+          @Override
+          public List<String> obfuscatedStackTrace() {
+            return ImmutableList.of("a.b.c.a");
+          }
+
+          @Override
+          public String mapping() {
+            return StringUtils.lines("com.android.tools.r8.R8 -> a.b.c:", "  boolean foo -> b");
+          }
+
+          @Override
+          public List<String> retracedStackTrace() {
+            return ImmutableList.of("com.android.tools.r8.R8.a");
+          }
+
+          @Override
+          public int expectedWarnings() {
+            return 0;
+          }
+        });
+  }
+
+  @Test
+  public void matchQualifiedFieldInTrace() {
+    runRetraceTest(
+        "%c\\.%f\\(%s\\)",
+        new StackTraceForTest() {
+          @Override
+          public List<String> obfuscatedStackTrace() {
+            return Collections.singletonList("a.b.c.d(Main.dummy)");
+          }
+
+          @Override
+          public List<String> retracedStackTrace() {
+            return Collections.singletonList("foo.Bar$Baz.baz(Bar.dummy)");
+          }
+
+          @Override
+          public String mapping() {
+            return StringUtils.lines(
+                "com.android.tools.r8.naming.retrace.Main -> a.b.c:",
+                "    int foo.Bar$Baz.baz -> d");
+          }
+
+          @Override
+          public int expectedWarnings() {
+            return 0;
+          }
+        });
+  }
+
+  @Test
+  public void matchSourceFileInContext() {
+    runRetraceTest(
+        "%c\\(%s\\)",
+        new StackTraceForTest() {
+          @Override
+          public List<String> obfuscatedStackTrace() {
+            return ImmutableList.of("a.b.c(SourceFile)");
+          }
+
+          @Override
+          public String mapping() {
+            return StringUtils.lines("com.android.tools.r8.R8 -> a.b.c:", "  boolean foo -> b");
+          }
+
+          @Override
+          public List<String> retracedStackTrace() {
+            return ImmutableList.of("com.android.tools.r8.R8(R8.java)");
+          }
+
+          @Override
+          public int expectedWarnings() {
+            return 0;
+          }
+        });
+  }
+
+  @Test
+  public void matchSourceFileInNoContext() {
+    runRetraceTest(
+        "%c\\(%s\\)",
+        new StackTraceForTest() {
+          @Override
+          public List<String> obfuscatedStackTrace() {
+            return ImmutableList.of("a.b.d(SourceFile)");
+          }
+
+          @Override
+          public String mapping() {
+            return StringUtils.lines("com.android.tools.r8.R8 -> a.b.c:", "  boolean foo -> b");
+          }
+
+          @Override
+          public List<String> retracedStackTrace() {
+            return ImmutableList.of("a.b.d(d.java)");
+          }
+
+          @Override
+          public int expectedWarnings() {
+            return 0;
+          }
+        });
+  }
+
+  @Test
+  public void testLineNumberInMethodContext() {
+    runRetraceTest(
+        "%c\\.%m\\(%l\\)",
+        new StackTraceForTest() {
+          @Override
+          public List<String> obfuscatedStackTrace() {
+            return ImmutableList.of("a.b.c.a(3)");
+          }
+
+          @Override
+          public String mapping() {
+            return StringUtils.lines(
+                "com.android.tools.r8.R8 -> a.b.c:", "  3:3:boolean foo():7 -> a");
+          }
+
+          @Override
+          public List<String> retracedStackTrace() {
+            return ImmutableList.of("com.android.tools.r8.R8.foo(7)");
+          }
+
+          @Override
+          public int expectedWarnings() {
+            return 0;
+          }
+        });
+  }
+
+  @Test
+  public void testNotFoundLineNumberInMethodContext() {
+    runRetraceTest(
+        "%c\\.%m\\(%l\\)",
+        new StackTraceForTest() {
+          @Override
+          public List<String> obfuscatedStackTrace() {
+            return ImmutableList.of("a.b.c.a()");
+          }
+
+          @Override
+          public String mapping() {
+            return StringUtils.lines(
+                "com.android.tools.r8.R8 -> a.b.c:", "  3:3:boolean foo():7 -> a");
+          }
+
+          @Override
+          public List<String> retracedStackTrace() {
+            return ImmutableList.of("a.b.c.a()");
+          }
+
+          @Override
+          public int expectedWarnings() {
+            return 0;
+          }
+        });
+  }
+
+  @Test
+  public void testNotFoundLineNumberInNoLineNumberMappingMethodContext() {
+    runRetraceTest(
+        "%c\\.%m\\(%l\\)",
+        new StackTraceForTest() {
+          @Override
+          public List<String> obfuscatedStackTrace() {
+            return ImmutableList.of("a.b.c.a(4)");
+          }
+
+          @Override
+          public String mapping() {
+            return StringUtils.lines("com.android.tools.r8.R8 -> a.b.c:", "  boolean foo() -> a");
+          }
+
+          @Override
+          public List<String> retracedStackTrace() {
+            return ImmutableList.of("com.android.tools.r8.R8.foo(4)");
+          }
+
+          @Override
+          public int expectedWarnings() {
+            return 0;
+          }
+        });
+  }
+
+  @Test
+  public void testPruningByLineNumber() {
+    runRetraceTest(
+        "%c\\.%m\\(%l\\)",
+        new StackTraceForTest() {
+          @Override
+          public List<String> obfuscatedStackTrace() {
+            return ImmutableList.of("a.b.c.a(3)");
+          }
+
+          @Override
+          public String mapping() {
+            return StringUtils.lines(
+                "com.android.tools.r8.R8 -> a.b.c:",
+                "  3:3:boolean foo():7 -> a",
+                "  4:4:boolean bar(int):8 -> a");
+          }
+
+          @Override
+          public List<String> retracedStackTrace() {
+            return ImmutableList.of("com.android.tools.r8.R8.foo(7)");
+          }
+
+          @Override
+          public int expectedWarnings() {
+            return 0;
+          }
+        });
+  }
+
+  @Test
+  public void matchOnType() {
+    runRetraceTest(
+        "%t",
+        new StackTraceForTest() {
+          @Override
+          public List<String> obfuscatedStackTrace() {
+            return ImmutableList.of("void", "a.a.a[]", "a.a.a[][][]");
+          }
+
+          @Override
+          public String mapping() {
+            return "";
+          }
+
+          @Override
+          public List<String> retracedStackTrace() {
+            return ImmutableList.of("void", "a.a.a[]", "a.a.a[][][]");
+          }
+
+          @Override
+          public int expectedWarnings() {
+            return 0;
+          }
+        });
+  }
+
+  @Test
+  public void matchOnFieldOrReturnType() {
+    runRetraceTest(
+        "%t %c.%m",
+        new StackTraceForTest() {
+          @Override
+          public List<String> obfuscatedStackTrace() {
+            return ImmutableList.of("void a.b.c.a", "a.a.a[] a.b.c.b");
+          }
+
+          @Override
+          public String mapping() {
+            return StringUtils.lines(
+                "com.android.tools.r8.D8 -> a.a.a:",
+                "com.android.tools.r8.R8 -> a.b.c:",
+                "  void foo() -> a",
+                "  com.android.tools.r8.D8[] bar() -> b");
+          }
+
+          @Override
+          public List<String> retracedStackTrace() {
+            return ImmutableList.of(
+                "void com.android.tools.r8.R8.foo",
+                "com.android.tools.r8.D8[] com.android.tools.r8.R8.bar");
+          }
+
+          @Override
+          public int expectedWarnings() {
+            return 0;
+          }
+        });
+  }
+
+  @Test
+  public void useReturnTypeToNarrowMethodMatches() {
+    runRetraceTest(
+        "%t %c.%m",
+        new StackTraceForTest() {
+          @Override
+          public List<String> obfuscatedStackTrace() {
+            return ImmutableList.of("void a.b.c.a", "a.a.a[] a.b.c.b");
+          }
+
+          @Override
+          public String mapping() {
+            return StringUtils.lines(
+                "com.android.tools.r8.D8 -> a.a.a:",
+                "com.android.tools.r8.R8 -> a.b.c:",
+                "  void foo() -> a",
+                "  int foo(int) -> a",
+                "  com.android.tools.r8.D8[] bar() -> b",
+                "  com.android.tools.r8.D8 bar(int) -> b");
+          }
+
+          @Override
+          public List<String> retracedStackTrace() {
+            return ImmutableList.of(
+                "void com.android.tools.r8.R8.foo",
+                "com.android.tools.r8.D8[] com.android.tools.r8.R8.bar");
+          }
+
+          @Override
+          public int expectedWarnings() {
+            return 0;
+          }
+        });
+  }
+
+  @Test
+  public void returnTypeCanMatchVoid() {
+    runRetraceTest(
+        "%t",
+        new StackTraceForTest() {
+          @Override
+          public List<String> obfuscatedStackTrace() {
+            return ImmutableList.of("void");
+          }
+
+          @Override
+          public String mapping() {
+            return "";
+          }
+
+          @Override
+          public List<String> retracedStackTrace() {
+            return ImmutableList.of("void");
+          }
+
+          @Override
+          public int expectedWarnings() {
+            return 0;
+          }
+        });
+  }
+
+  @Test
+  public void testArguments() {
+    runRetraceTest(
+        "%c.%m\\(%a\\)",
+        new StackTraceForTest() {
+          @Override
+          public List<String> obfuscatedStackTrace() {
+            return ImmutableList.of("a.b.c.a(int,a.a.a[],boolean)");
+          }
+
+          @Override
+          public String mapping() {
+            return StringUtils.lines(
+                "com.android.tools.r8.D8 -> a.a.a:",
+                "com.android.tools.r8.R8 -> a.b.c:",
+                "  void foo(int,com.android.tools.r8.D8[],boolean) -> a");
+          }
+
+          @Override
+          public List<String> retracedStackTrace() {
+            return ImmutableList.of(
+                "com.android.tools.r8.R8.foo(int,com.android.tools.r8.D8[],boolean)");
+          }
+
+          @Override
+          public int expectedWarnings() {
+            return 0;
+          }
+        });
+  }
+
+  @Test
+  public void testNoArguments() {
+    runRetraceTest(
+        "%c.%m\\(%a\\)",
+        new StackTraceForTest() {
+          @Override
+          public List<String> obfuscatedStackTrace() {
+            return ImmutableList.of("a.b.c.a()");
+          }
+
+          @Override
+          public String mapping() {
+            return StringUtils.lines(
+                "com.android.tools.r8.D8 -> a.a.a:",
+                "com.android.tools.r8.R8 -> a.b.c:",
+                "  void foo() -> a");
+          }
+
+          @Override
+          public List<String> retracedStackTrace() {
+            return ImmutableList.of("com.android.tools.r8.R8.foo()");
+          }
+
+          @Override
+          public int expectedWarnings() {
+            return 0;
+          }
+        });
+  }
+
+  @Test
+  public void testPruningOfMethodsByFormals() {
+    runRetraceTest(
+        "%c.%m\\(%a\\)",
+        new StackTraceForTest() {
+          @Override
+          public List<String> obfuscatedStackTrace() {
+            return ImmutableList.of("a.b.c.a(a.a.a)");
+          }
+
+          @Override
+          public String mapping() {
+            return StringUtils.lines(
+                "com.android.tools.r8.D8 -> a.a.a:",
+                "com.android.tools.r8.R8 -> a.b.c:",
+                "  void foo() -> a",
+                "  void bar(com.android.tools.r8.D8) -> a");
+          }
+
+          @Override
+          public List<String> retracedStackTrace() {
+            return ImmutableList.of("com.android.tools.r8.R8.bar(com.android.tools.r8.D8)");
+          }
+
+          @Override
+          public int expectedWarnings() {
+            return 0;
+          }
+        });
+  }
+
+  @Test
+  public void matchOnSimpleStackTrace() {
+    runRetraceTest(
+        DEFAULT_REGULAR_EXPRESSION,
+        new StackTraceForTest() {
+          @Override
+          public List<String> obfuscatedStackTrace() {
+            return ImmutableList.of(
+                "com.android.tools.r8.a: foo bar baz",
+                "  at com.android.tools.r8.b.a(SourceFile)",
+                "  at a.c.a.b(SourceFile)");
+          }
+
+          @Override
+          public String mapping() {
+            return StringUtils.joinLines(
+                "com.android.tools.r8.R8 -> com.android.tools.r8.a:",
+                "com.android.tools.r8.Bar -> com.android.tools.r8.b:",
+                "  void foo() -> a",
+                "com.android.tools.r8.Baz -> a.c.a:",
+                "  void bar() -> b");
+          }
+
+          @Override
+          public List<String> retracedStackTrace() {
+            return ImmutableList.of(
+                "com.android.tools.r8.R8: foo bar baz",
+                "  at com.android.tools.r8.Bar.foo(Bar.java)",
+                "  at com.android.tools.r8.Baz.bar(Baz.java)");
+          }
+
+          @Override
+          public int expectedWarnings() {
+            return 0;
+          }
+        });
+  }
+
+  private TestDiagnosticMessagesImpl runRetraceTest(
+      String regularExpression, StackTraceForTest stackTraceForTest) {
+    TestDiagnosticMessagesImpl diagnosticsHandler = new TestDiagnosticMessagesImpl();
+    RetraceCommand retraceCommand =
+        RetraceCommand.builder(diagnosticsHandler)
+            .setProguardMapProducer(stackTraceForTest::mapping)
+            .setStackTrace(stackTraceForTest.obfuscatedStackTrace())
+            .setRetracedStackTraceConsumer(
+                retraced -> {
+                  assertEquals(stackTraceForTest.retracedStackTrace(), retraced);
+                })
+            .setRegularExpression(regularExpression)
+            .build();
+    Retrace.run(retraceCommand);
+    return diagnosticsHandler;
+  }
+}