Introduce new retrace api for general consumption

This is the main CL for introducing a new retrace API that is no
longer tied to stack-traces. Since the entry-points are defined in
terms of references, users can abstract away from string
representations of classes and members.

Bug: 144151634
Change-Id: I8d46e485e9e725d63732634ffdf592422f037ba8
diff --git a/src/main/java/com/android/tools/r8/naming/ClassNamingForNameMapper.java b/src/main/java/com/android/tools/r8/naming/ClassNamingForNameMapper.java
index 226a5f9..1a1d4ef 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNamingForNameMapper.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNamingForNameMapper.java
@@ -88,7 +88,7 @@
   public static class MappedRangesOfName {
     private final List<MappedRange> mappedRanges;
 
-    MappedRangesOfName(List<MappedRange> mappedRanges) {
+    public MappedRangesOfName(List<MappedRange> mappedRanges) {
       this.mappedRanges = mappedRanges;
     }
 
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 054917e..95fbcc8 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,22 @@
       return name.indexOf(JAVA_PACKAGE_SEPARATOR) != -1;
     }
 
+    public boolean isMethodSignature() {
+      return false;
+    }
+
+    public boolean isFieldSignature() {
+      return false;
+    }
+
+    public MethodSignature asMethodSignature() {
+      return null;
+    }
+
+    public FieldSignature asFieldSignature() {
+      return null;
+    }
+
     @Override
     public String toString() {
       try {
@@ -206,6 +222,16 @@
       writer.append(' ');
       writer.append(name);
     }
+
+    @Override
+    public boolean isFieldSignature() {
+      return true;
+    }
+
+    @Override
+    public FieldSignature asFieldSignature() {
+      return this;
+    }
   }
 
   public static class MethodSignature extends Signature {
@@ -265,7 +291,7 @@
       return name.substring(name.lastIndexOf(JAVA_PACKAGE_SEPARATOR) + 1);
     }
 
-    public String toUnqualifiedHolder() {
+    public String toHolderFromQualified() {
       assert isQualified();
       return name.substring(0, name.lastIndexOf(JAVA_PACKAGE_SEPARATOR));
     }
@@ -348,5 +374,15 @@
       sb.append(javaTypeToDescriptor(type));
       return sb.toString();
     }
+
+    @Override
+    public boolean isMethodSignature() {
+      return true;
+    }
+
+    @Override
+    public MethodSignature asMethodSignature() {
+      return this;
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/references/FieldReference.java b/src/main/java/com/android/tools/r8/references/FieldReference.java
index d4d74fd..6f27753 100644
--- a/src/main/java/com/android/tools/r8/references/FieldReference.java
+++ b/src/main/java/com/android/tools/r8/references/FieldReference.java
@@ -13,15 +13,19 @@
  * type of the field.
  */
 @Keep
-public final class FieldReference {
+public class FieldReference {
   private final ClassReference holderClass;
   private final String fieldName;
   private final TypeReference fieldType;
 
+  boolean isUnknown() {
+    return false;
+  }
+
   FieldReference(ClassReference holderClass, String fieldName, TypeReference fieldType) {
     assert holderClass != null;
     assert fieldName != null;
-    assert fieldType != null;
+    assert fieldType != null || isUnknown();
     this.holderClass = holderClass;
     this.fieldName = fieldName;
     this.fieldType = fieldType;
@@ -65,4 +69,29 @@
   public String toString() {
     return getHolderClass().toString() + getFieldName() + ":" + getFieldType().getDescriptor();
   }
+
+  public static final class UnknownFieldReference extends FieldReference {
+
+    private final ClassReference holderClass;
+    private final String fieldName;
+
+    public UnknownFieldReference(ClassReference holderClass, String fieldName) {
+      super(holderClass, fieldName, null);
+      this.holderClass = holderClass;
+      this.fieldName = fieldName;
+    }
+
+    public ClassReference getHolderClass() {
+      return holderClass;
+    }
+
+    public String getFieldName() {
+      return fieldName;
+    }
+
+    @Override
+    boolean isUnknown() {
+      return true;
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/references/MethodReference.java b/src/main/java/com/android/tools/r8/references/MethodReference.java
index 0a89715..b4a8116 100644
--- a/src/main/java/com/android/tools/r8/references/MethodReference.java
+++ b/src/main/java/com/android/tools/r8/references/MethodReference.java
@@ -17,7 +17,7 @@
  * full list of formal parameters.
  */
 @Keep
-public final class MethodReference {
+public class MethodReference {
   private final ClassReference holderClass;
   private final String methodName;
   private final List<TypeReference> formalTypes;
@@ -30,13 +30,17 @@
       TypeReference returnType) {
     assert holderClass != null;
     assert methodName != null;
-    assert formalTypes != null;
+    assert formalTypes != null || isUnknown();
     this.holderClass = holderClass;
     this.methodName = methodName;
     this.formalTypes = formalTypes;
     this.returnType = returnType;
   }
 
+  public boolean isUnknown() {
+    return false;
+  }
+
   public ClassReference getHolderClass() {
     return holderClass;
   }
@@ -86,4 +90,29 @@
   public String toString() {
     return getHolderClass().toString() + getMethodName() + getMethodDescriptor();
   }
+
+  public static final class UnknownMethodReference extends MethodReference {
+
+    private final ClassReference holderClass;
+    private final String methodName;
+
+    @Override
+    public boolean isUnknown() {
+      return true;
+    }
+
+    public UnknownMethodReference(ClassReference holderClass, String methodName) {
+      super(holderClass, methodName, null, null);
+      this.holderClass = holderClass;
+      this.methodName = methodName;
+    }
+
+    public ClassReference getHolderClass() {
+      return holderClass;
+    }
+
+    public String getMethodName() {
+      return methodName;
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/references/Reference.java b/src/main/java/com/android/tools/r8/references/Reference.java
index 94d4ea0..dcf8c68 100644
--- a/src/main/java/com/android/tools/r8/references/Reference.java
+++ b/src/main/java/com/android/tools/r8/references/Reference.java
@@ -65,6 +65,10 @@
     return instance;
   }
 
+  public static TypeReference returnTypeFromDescriptor(String descriptor) {
+    return descriptor.equals("V") ? null : typeFromDescriptor(descriptor);
+  }
+
   public static TypeReference typeFromDescriptor(String descriptor) {
     switch (descriptor.charAt(0)) {
       case 'L':
@@ -76,6 +80,10 @@
     }
   }
 
+  public static TypeReference typeFromTypeName(String typeName) {
+    return typeFromDescriptor(DescriptorUtils.javaTypeToDescriptor(typeName));
+  }
+
   // Internal helper to convert Class<?> for primitive/array types too.
   private static TypeReference typeFromClass(Class<?> clazz) {
     return typeFromDescriptor(DescriptorUtils.javaTypeToDescriptor(clazz.getTypeName()));
diff --git a/src/main/java/com/android/tools/r8/retrace/Result.java b/src/main/java/com/android/tools/r8/retrace/Result.java
new file mode 100644
index 0000000..a8e3903
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/retrace/Result.java
@@ -0,0 +1,14 @@
+// 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.Keep;
+import java.util.function.Consumer;
+
+@Keep
+public abstract class Result<R, RR extends Result<R, RR>> {
+
+  public abstract RR apply(Consumer<R> resultConsumer);
+}
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceBase.java b/src/main/java/com/android/tools/r8/retrace/RetraceBase.java
new file mode 100644
index 0000000..c5284ea
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceBase.java
@@ -0,0 +1,26 @@
+// 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.references.ClassReference;
+import com.android.tools.r8.references.FieldReference;
+import com.android.tools.r8.references.MethodReference;
+
+public interface RetraceBase {
+
+  RetraceMethodResult retrace(MethodReference methodReference);
+
+  RetraceFieldResult retrace(FieldReference fieldReference);
+
+  RetraceClassResult retrace(ClassReference classReference);
+
+  String retraceSourceFile(ClassReference classReference, String sourceFile);
+
+  String retraceSourceFile(
+      ClassReference classReference,
+      String sourceFile,
+      ClassReference retracedClassReference,
+      boolean hasRetraceResult);
+}
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceBaseImpl.java b/src/main/java/com/android/tools/r8/retrace/RetraceBaseImpl.java
new file mode 100644
index 0000000..19bc557
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceBaseImpl.java
@@ -0,0 +1,88 @@
+// 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.naming.ClassNameMapper;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.FieldReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.utils.Box;
+import com.google.common.collect.Sets;
+import com.google.common.io.Files;
+import java.util.Set;
+
+public class RetraceBaseImpl implements RetraceBase {
+
+  private static final Set<String> UNKNOWN_SOURCEFILE_NAMES =
+      Sets.newHashSet("", "SourceFile", "Unknown", "Unknown Source");
+
+  private final ClassNameMapper classNameMapper;
+
+  RetraceBaseImpl(ClassNameMapper classNameMapper) {
+    this.classNameMapper = classNameMapper;
+  }
+
+  @Override
+  public RetraceMethodResult retrace(MethodReference methodReference) {
+    return retrace(methodReference.getHolderClass()).lookupMethod(methodReference.getMethodName());
+  }
+
+  @Override
+  public RetraceFieldResult retrace(FieldReference fieldReference) {
+    return retrace(fieldReference.getHolderClass()).lookupField(fieldReference.getFieldName());
+  }
+
+  @Override
+  public RetraceClassResult retrace(ClassReference classReference) {
+    return RetraceClassResult.create(
+        classReference, classNameMapper.getClassNaming(classReference.getTypeName()));
+  }
+
+  @Override
+  public String retraceSourceFile(ClassReference classReference, String sourceFile) {
+    Box<String> retracedSourceFile = new Box<>();
+    retrace(classReference)
+        .apply(element -> retracedSourceFile.set(element.retraceSourceFile(sourceFile, this)));
+    return retracedSourceFile.get();
+  }
+
+  @Override
+  public String retraceSourceFile(
+      ClassReference obfuscatedClass,
+      String sourceFile,
+      ClassReference retracedClassReference,
+      boolean hasRetraceResult) {
+    boolean fileNameProbablyChanged =
+        hasRetraceResult
+            && !retracedClassReference.getTypeName().startsWith(obfuscatedClass.getTypeName());
+    if (!UNKNOWN_SOURCEFILE_NAMES.contains(sourceFile) && !fileNameProbablyChanged) {
+      // We have no new information, only rewrite filename if it is unknown.
+      // PG-retrace will always rewrite the filename, but that seems a bit to harsh to do.
+      return sourceFile;
+    }
+    if (!hasRetraceResult) {
+      // We have no mapping but but the file name is unknown, so the best we can do is take the
+      // name of the obfuscated clazz.
+      assert obfuscatedClass.getTypeName().equals(retracedClassReference.getTypeName());
+      return getClassSimpleName(obfuscatedClass.getTypeName()) + ".java";
+    }
+    String newFileName = getClassSimpleName(retracedClassReference.getTypeName());
+    String extension = Files.getFileExtension(sourceFile);
+    if (extension.isEmpty()) {
+      extension = "java";
+    }
+    return newFileName + "." + extension;
+  }
+
+  private static String getClassSimpleName(String clazz) {
+    int lastIndexOfPeriod = clazz.lastIndexOf('.');
+    // Check if we can find a subclass separator.
+    int endIndex = clazz.lastIndexOf('$');
+    if (lastIndexOfPeriod > endIndex || endIndex < 0) {
+      endIndex = clazz.length();
+    }
+    return clazz.substring(lastIndexOfPeriod + 1, endIndex);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceClassResult.java b/src/main/java/com/android/tools/r8/retrace/RetraceClassResult.java
new file mode 100644
index 0000000..a0d5076
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceClassResult.java
@@ -0,0 +1,124 @@
+// 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.Keep;
+import com.android.tools.r8.naming.ClassNamingForNameMapper;
+import com.android.tools.r8.naming.ClassNamingForNameMapper.MappedRangesOfName;
+import com.android.tools.r8.naming.MemberNaming;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.retrace.RetraceClassResult.Element;
+import com.android.tools.r8.utils.Box;
+import java.util.List;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+
+@Keep
+public class RetraceClassResult extends Result<Element, RetraceClassResult> {
+
+  private final ClassReference obfuscatedReference;
+  private final ClassNamingForNameMapper mapper;
+
+  private RetraceClassResult(ClassReference obfuscatedReference, ClassNamingForNameMapper mapper) {
+    this.obfuscatedReference = obfuscatedReference;
+    this.mapper = mapper;
+  }
+
+  static RetraceClassResult create(
+      ClassReference obfuscatedReference, ClassNamingForNameMapper mapper) {
+    return new RetraceClassResult(obfuscatedReference, mapper);
+  }
+
+  public RetraceFieldResult lookupField(String fieldName) {
+    return lookup(
+        fieldName,
+        (mapper, name) -> {
+          List<MemberNaming> memberNamings = mapper.mappedNamingsByName.get(name);
+          if (memberNamings == null || memberNamings.isEmpty()) {
+            return null;
+          }
+          return memberNamings;
+        },
+        RetraceFieldResult::new);
+  }
+
+  public RetraceMethodResult lookupMethod(String methodName) {
+    return lookup(
+        methodName,
+        (mapper, name) -> {
+          MappedRangesOfName mappedRanges = mapper.mappedRangesByRenamedName.get(name);
+          if (mappedRanges == null || mappedRanges.getMappedRanges().isEmpty()) {
+            return null;
+          }
+          return mappedRanges;
+        },
+        RetraceMethodResult::new);
+  }
+
+  private <T, R> R lookup(
+      String name,
+      BiFunction<ClassNamingForNameMapper, String, T> lookupFunction,
+      ResultConstructor<T, R> constructor) {
+    Box<R> elementBox = new Box<>();
+    apply(
+        element -> {
+          assert !elementBox.isSet();
+          T mappedRangesForT = null;
+          if (element.mapper != null) {
+            mappedRangesForT = lookupFunction.apply(element.mapper, name);
+          }
+          elementBox.set(constructor.create(element, mappedRangesForT, name));
+        });
+    return elementBox.get();
+  }
+
+  private boolean hasRetraceResult() {
+    return mapper != null;
+  }
+
+  @Override
+  public RetraceClassResult apply(Consumer<Element> resultConsumer) {
+    resultConsumer.accept(
+        new Element(
+            this,
+            mapper == null ? obfuscatedReference : Reference.classFromTypeName(mapper.originalName),
+            mapper));
+    return this;
+  }
+
+  private interface ResultConstructor<T, R> {
+    R create(Element element, T mappings, String obfuscatedName);
+  }
+
+  public static class Element {
+
+    private final RetraceClassResult classResult;
+    private final ClassReference classReference;
+    private final ClassNamingForNameMapper mapper;
+
+    public Element(
+        RetraceClassResult classResult,
+        ClassReference classReference,
+        ClassNamingForNameMapper mapper) {
+      this.classResult = classResult;
+      this.classReference = classReference;
+      this.mapper = mapper;
+    }
+
+    public ClassReference getClassReference() {
+      return classReference;
+    }
+
+    public RetraceClassResult getRetraceClassResult() {
+      return classResult;
+    }
+
+    public String retraceSourceFile(String fileName, RetraceBase retraceBase) {
+      return retraceBase.retraceSourceFile(
+          classResult.obfuscatedReference, fileName, classReference, mapper != null);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceCore.java b/src/main/java/com/android/tools/r8/retrace/RetraceCore.java
index 8db2ace..d895d8d 100644
--- a/src/main/java/com/android/tools/r8/retrace/RetraceCore.java
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceCore.java
@@ -386,7 +386,7 @@
         String mappedClazz = retraceClazz;
         String mappedMethod = mappedRange.signature.name;
         if (mappedRange.signature.isQualified()) {
-          mappedClazz = mappedRange.signature.toUnqualifiedHolder();
+          mappedClazz = mappedRange.signature.toHolderFromQualified();
           mappedMethod = mappedRange.signature.toUnqualifiedName();
         }
         int retracedLinePosition = linePosition;
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceFieldResult.java b/src/main/java/com/android/tools/r8/retrace/RetraceFieldResult.java
new file mode 100644
index 0000000..6e0d232
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceFieldResult.java
@@ -0,0 +1,101 @@
+// 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.Keep;
+import com.android.tools.r8.naming.MemberNaming;
+import com.android.tools.r8.naming.MemberNaming.FieldSignature;
+import com.android.tools.r8.references.FieldReference;
+import com.android.tools.r8.references.FieldReference.UnknownFieldReference;
+import com.android.tools.r8.references.Reference;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+@Keep
+public class RetraceFieldResult extends Result<RetraceFieldResult.Element, RetraceFieldResult> {
+
+  private final RetraceClassResult.Element classElement;
+  private final List<MemberNaming> memberNamings;
+  private final String obfuscatedName;
+
+  RetraceFieldResult(
+      RetraceClassResult.Element classElement,
+      List<MemberNaming> memberNamings,
+      String obfuscatedName) {
+    this.classElement = classElement;
+    this.memberNamings = memberNamings;
+    this.obfuscatedName = obfuscatedName;
+    assert classElement != null;
+    assert memberNamings == null
+        || (!memberNamings.isEmpty() && memberNamings.stream().allMatch(Objects::nonNull));
+  }
+
+  private boolean hasRetraceResult() {
+    return memberNamings != null;
+  }
+
+  private boolean isAmbiguous() {
+    if (!hasRetraceResult()) {
+      return false;
+    }
+    assert memberNamings != null;
+    return memberNamings.size() > 1;
+  }
+
+  @Override
+  public RetraceFieldResult apply(Consumer<Element> resultConsumer) {
+    if (hasRetraceResult()) {
+      assert !memberNamings.isEmpty();
+      for (MemberNaming memberNaming : memberNamings) {
+        assert memberNaming.isFieldNaming();
+        FieldSignature fieldSignature = memberNaming.getOriginalSignature().asFieldSignature();
+        resultConsumer.accept(
+            new Element(
+                this,
+                classElement,
+                Reference.field(
+                    classElement.getClassReference(),
+                    fieldSignature.name,
+                    Reference.typeFromTypeName(fieldSignature.type))));
+      }
+    } else {
+      resultConsumer.accept(
+          new Element(
+              this,
+              classElement,
+              new UnknownFieldReference(classElement.getClassReference(), obfuscatedName)));
+    }
+    return this;
+  }
+
+  public static class Element {
+
+    private final FieldReference fieldReference;
+    private final RetraceFieldResult retraceFieldResult;
+    private final RetraceClassResult.Element classElement;
+
+    private Element(
+        RetraceFieldResult retraceFieldResult,
+        RetraceClassResult.Element classElement,
+        FieldReference fieldReference) {
+      this.classElement = classElement;
+      this.fieldReference = fieldReference;
+      this.retraceFieldResult = retraceFieldResult;
+    }
+
+    public FieldReference getFieldReference() {
+      return fieldReference;
+    }
+
+    public RetraceFieldResult getRetraceFieldResult() {
+      return getRetraceFieldResult();
+    }
+
+    public RetraceClassResult.Element getClassElement() {
+      return classElement;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceMethodResult.java b/src/main/java/com/android/tools/r8/retrace/RetraceMethodResult.java
new file mode 100644
index 0000000..e727a6a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceMethodResult.java
@@ -0,0 +1,159 @@
+// 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.Keep;
+import com.android.tools.r8.naming.ClassNamingForNameMapper.MappedRange;
+import com.android.tools.r8.naming.ClassNamingForNameMapper.MappedRangesOfName;
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.naming.Range;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.MethodReference.UnknownMethodReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.references.TypeReference;
+import com.android.tools.r8.utils.DescriptorUtils;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+@Keep
+public class RetraceMethodResult extends Result<RetraceMethodResult.Element, RetraceMethodResult> {
+
+  private final String obfuscatedName;
+  private final RetraceClassResult.Element classElement;
+  private final MappedRangesOfName mappedRanges;
+  private Boolean isAmbiguousCached = null;
+
+  RetraceMethodResult(
+      RetraceClassResult.Element classElement,
+      MappedRangesOfName mappedRanges,
+      String obfuscatedName) {
+    this.classElement = classElement;
+    this.mappedRanges = mappedRanges;
+    this.obfuscatedName = obfuscatedName;
+    assert classElement != null;
+  }
+
+  private boolean hasRetraceResult() {
+    return mappedRanges != null && mappedRanges.getMappedRanges().size() > 0;
+  }
+
+  public boolean isAmbiguous() {
+    if (isAmbiguousCached != null) {
+      return isAmbiguousCached;
+    }
+    if (!hasRetraceResult()) {
+      return false;
+    }
+    assert mappedRanges != null;
+    Range minifiedRange = null;
+    boolean seenNull = false;
+    for (MappedRange mappedRange : mappedRanges.getMappedRanges()) {
+      if (minifiedRange != null && !minifiedRange.equals(mappedRange.minifiedRange)) {
+        isAmbiguousCached = true;
+        return true;
+      } else if (minifiedRange == null) {
+        if (seenNull) {
+          isAmbiguousCached = true;
+          return true;
+        }
+        seenNull = true;
+      }
+      minifiedRange = mappedRange.minifiedRange;
+    }
+    isAmbiguousCached = false;
+    return false;
+  }
+
+  public RetraceMethodResult narrowByLine(int linePosition) {
+    if (!hasRetraceResult()) {
+      return this;
+    }
+    List<MappedRange> narrowedRanges = this.mappedRanges.allRangesForLine(linePosition, false);
+    if (narrowedRanges.isEmpty()) {
+      narrowedRanges = new ArrayList<>();
+      for (MappedRange mappedRange : this.mappedRanges.getMappedRanges()) {
+        if (mappedRange.minifiedRange == null) {
+          narrowedRanges.add(mappedRange);
+        }
+      }
+    }
+    return new RetraceMethodResult(
+        classElement, new MappedRangesOfName(narrowedRanges), obfuscatedName);
+  }
+
+  @Override
+  public RetraceMethodResult apply(Consumer<Element> resultConsumer) {
+    if (hasRetraceResult()) {
+      for (MappedRange mappedRange : mappedRanges.getMappedRanges()) {
+        MethodSignature signature = mappedRange.signature;
+        ClassReference holder =
+            mappedRange.signature.isQualified()
+                ? Reference.classFromDescriptor(
+                    DescriptorUtils.javaTypeToDescriptor(
+                        mappedRange.signature.toHolderFromQualified()))
+                : classElement.getClassReference();
+        List<TypeReference> formalTypes = new ArrayList<>(signature.parameters.length);
+        for (String parameter : signature.parameters) {
+          formalTypes.add(Reference.typeFromTypeName(parameter));
+        }
+        TypeReference returnType =
+            Reference.returnTypeFromDescriptor(
+                DescriptorUtils.javaTypeToDescriptor(signature.type));
+        MethodReference retracedMethod =
+            Reference.method(
+                holder,
+                signature.isQualified() ? signature.toUnqualifiedName() : signature.name,
+                formalTypes,
+                returnType);
+        resultConsumer.accept(new Element(this, classElement, retracedMethod, mappedRange));
+      }
+    } else {
+      resultConsumer.accept(
+          new Element(
+              this,
+              classElement,
+              new UnknownMethodReference(classElement.getClassReference(), obfuscatedName),
+              null));
+    }
+    return this;
+  }
+
+  public static class Element {
+
+    private final MethodReference methodReference;
+    private final RetraceMethodResult retraceMethodResult;
+    private final RetraceClassResult.Element classElement;
+    private final MappedRange mappedRange;
+
+    private Element(
+        RetraceMethodResult retraceMethodResult,
+        RetraceClassResult.Element classElement,
+        MethodReference methodReference,
+        MappedRange mappedRange) {
+      this.classElement = classElement;
+      this.retraceMethodResult = retraceMethodResult;
+      this.methodReference = methodReference;
+      this.mappedRange = mappedRange;
+    }
+
+    public MethodReference getMethodReference() {
+      return methodReference;
+    }
+
+    public RetraceMethodResult getRetraceMethodResult() {
+      return retraceMethodResult;
+    }
+
+    public RetraceClassResult.Element getClassElement() {
+      return classElement;
+    }
+
+    public int getOriginalLineNumber(int linePosition) {
+      return mappedRange != null ? mappedRange.getOriginalLineNumber(linePosition) : linePosition;
+    }
+  }
+}