[Compose] Compose inlined outlines into outline callsites

Bug: b/284925475
Change-Id: Ib5f55f95849eaac319e8d8102168d5d057e3e75a
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 76a188c..da66dfd 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNamingForNameMapper.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNamingForNameMapper.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.naming.MemberNaming.Signature.SignatureKind;
 import com.android.tools.r8.naming.mappinginformation.MappingInformation;
 import com.android.tools.r8.naming.mappinginformation.OutlineCallsiteMappingInformation;
+import com.android.tools.r8.naming.mappinginformation.OutlineMappingInformation;
 import com.android.tools.r8.naming.mappinginformation.RewriteFrameMappingInformation;
 import com.android.tools.r8.utils.ChainableStringConsumer;
 import com.android.tools.r8.utils.CollectionUtils;
@@ -632,6 +633,15 @@
           MappingInformation::asRewriteFrameMappingInformation);
     }
 
+    public OutlineMappingInformation getOutlineMappingInformation() {
+      List<OutlineMappingInformation> outlineMappingInformation =
+          filter(
+              MappingInformation::isOutlineMappingInformation,
+              MappingInformation::asOutlineMappingInformation);
+      assert outlineMappingInformation.size() <= 1;
+      return outlineMappingInformation.isEmpty() ? null : outlineMappingInformation.get(0);
+    }
+
     public int getOriginalLineNumber(int lineNumberAfterMinification) {
       if (minifiedRange == null) {
         // General mapping without concrete line numbers: "a() -> b"
@@ -765,6 +775,11 @@
       return Collections.unmodifiableList(additionalMappingInformation);
     }
 
+    public void setAdditionalMappingInformationInternal(
+        List<MappingInformation> mappingInformation) {
+      this.additionalMappingInformation = mappingInformation;
+    }
+
     public MappedRange partitionOnMinifiedRange(Range minifiedRange) {
       if (minifiedRange.equals(this.minifiedRange)) {
         return this;
@@ -784,5 +799,11 @@
     public boolean isOriginalRangePreamble() {
       return originalRange != null && originalRange.isPreamble();
     }
+
+    public MappedRange withMinifiedRange(Range newMinifiedRange) {
+      return newMinifiedRange.equals(minifiedRange)
+          ? this
+          : new MappedRange(newMinifiedRange, signature, originalRange, renamedName);
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/naming/ComposingBuilder.java b/src/main/java/com/android/tools/r8/naming/ComposingBuilder.java
index b272aca..d87e036 100644
--- a/src/main/java/com/android/tools/r8/naming/ComposingBuilder.java
+++ b/src/main/java/com/android/tools/r8/naming/ComposingBuilder.java
@@ -27,6 +27,7 @@
 import com.android.tools.r8.utils.Box;
 import com.android.tools.r8.utils.ChainableStringConsumer;
 import com.android.tools.r8.utils.ConsumerUtils;
+import com.android.tools.r8.utils.IntBox;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.Pair;
@@ -42,12 +43,14 @@
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Objects;
 import java.util.Set;
 import java.util.TreeMap;
+import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 
 public class ComposingBuilder {
@@ -640,7 +643,10 @@
             if (composedInlineFrames.isEmpty()) {
               splitOnNewMinifiedRange(
                   composeMappedRangesForMethod(
-                      existingMappedRanges, mappedRange, computedOutlineInformation),
+                      existingClassBuilder,
+                      existingMappedRanges,
+                      mappedRange,
+                      computedOutlineInformation),
                   Collections.emptyList(),
                   newComposedInlineFrames::add);
             } else {
@@ -649,7 +655,10 @@
                     mappedRange.partitionOnMinifiedRange(composedInlineFrame.get(0).minifiedRange);
                 splitOnNewMinifiedRange(
                     composeMappedRangesForMethod(
-                        existingMappedRanges, splitMappedRange, computedOutlineInformation),
+                        existingClassBuilder,
+                        existingMappedRanges,
+                        splitMappedRange,
+                        computedOutlineInformation),
                     composedInlineFrame,
                     newComposedInlineFrames::add);
               }
@@ -662,86 +671,195 @@
               composedInlineFrames = Collections.emptyList();
             }
           }
-          MappedRange lastComposedRange = ListUtils.last(composedRanges);
-          if (computedOutlineInformation.seenOutlineMappingInformation != null) {
+          // Check if we could have inlined an outline which is true if we see both an outline and
+          // call site to patch up.
+          if (!computedOutlineInformation.seenOutlineMappingInformation.isEmpty()
+              && !computedOutlineInformation.outlineCallsiteMappingInformationToPatchUp.isEmpty()) {
+            Set<OutlineCallsiteMappingInformation> outlineCallSitesToRemove =
+                Sets.newIdentityHashSet();
+            Set<OutlineMappingInformation> outlinesToRemove = Sets.newIdentityHashSet();
+            // We patch up all ranges from top to bottom with the invariant that all at a given
+            // index, all above have been updated correctly. We will do expansion of frames
+            // when separating out single minified lines, but we keep the outline information
+            // present such that we can fix them when seeing them later.
+            int composedRangeIndex = 0;
+            while (composedRangeIndex < composedRanges.size() - 1) {
+              MappedRange outline = composedRanges.get(composedRangeIndex++);
+              if (outline.isOutlineFrame()
+                  && outline.minifiedRange.equals(
+                      composedRanges.get(composedRangeIndex).minifiedRange)) {
+                // We should replace the inlined outline frame positions with the synthesized
+                // positions from the outline call site.
+                MappedRange outlineCallSite = composedRanges.get(composedRangeIndex);
+                if (outlineCallSite.getOutlineCallsiteInformation().size() != 1) {
+                  // If we have an inlined outline it must be such that the outer frame is an
+                  // outline callsite.
+                  throw new MappingComposeException(
+                      "Expected exactly one outline call site for a mapped range with signature '"
+                          + outlineCallSite.getOriginalSignature()
+                          + "'.");
+                }
+                OutlineCallsiteMappingInformation outlineCallSiteInformation =
+                    outlineCallSite.getOutlineCallsiteInformation().get(0);
+                // The original positions in the outline callsite have been composed, so we have to
+                // find the existing mapped range and iterate the original positions for that range.
+                ComputedMappedRangeForOutline computedInformationForCallSite =
+                    computedOutlineInformation.getComputedRange(
+                        outlineCallSiteInformation, outlineCallSite);
+                if (computedInformationForCallSite == null) {
+                  continue;
+                }
+                Map<Integer, List<MappedRange>> mappedRangesForOutline =
+                    new HashMap<>(outlineCallSiteInformation.getPositions().size());
+                visitOutlineMappedPositions(
+                    outlineCallSiteInformation,
+                    computedInformationForCallSite
+                        .current
+                        .getOriginalSignature()
+                        .asMethodSignature(),
+                    mappedRangesForOutline::put);
+                List<MappedRange> newComposedRanges = new ArrayList<>();
+                // Copy all previous handled mapped ranges into a new list.
+                for (MappedRange previousMappedRanges : composedRanges) {
+                  if (previousMappedRanges == outline) {
+                    break;
+                  }
+                  newComposedRanges.add(previousMappedRanges);
+                }
+                // The original positions in the outline have been composed, so we have to find the
+                // existing mapped range and iterate the original positions for that range.
+                ComputedMappedRangeForOutline computedInformationForOutline =
+                    computedOutlineInformation.getComputedRange(
+                        outline.getOutlineMappingInformation(), outline);
+                if (computedInformationForOutline == null) {
+                  continue;
+                }
+                // The outline could have additional inlined positions in it, but we should be
+                // guaranteed to find call site information on all original line numbers. We
+                // therefore iterate one by one and amend the subsequent outer frames as well.
+                MappedRange current = computedInformationForOutline.current;
+                int minifiedLine = outline.minifiedRange.from;
+                for (int originalLine = current.getOriginalRangeOrIdentity().from;
+                    originalLine <= current.getOriginalRangeOrIdentity().to;
+                    originalLine++) {
+                  // If the outline is itself an inline frame it is bound to only have one original
+                  // position and we can simply insert all inline frames on that position with the
+                  // existing minified range.
+                  Range newMinifiedRange =
+                      outline.originalRange.isCardinal
+                          ? outline.minifiedRange
+                          : new Range(minifiedLine, minifiedLine);
+                  List<MappedRange> outlineMappedRanges = mappedRangesForOutline.get(originalLine);
+                  if (outlineMappedRanges != null) {
+                    outlineMappedRanges.forEach(
+                        range -> {
+                          if (range != ListUtils.last(outlineMappedRanges)) {
+                            newComposedRanges.add(
+                                new MappedRange(
+                                    newMinifiedRange,
+                                    range.getOriginalSignature().asMethodSignature(),
+                                    range.originalRange,
+                                    outlineCallSite.getRenamedName()));
+                          }
+                        });
+                    newComposedRanges.add(
+                        new MappedRange(
+                            newMinifiedRange,
+                            outlineCallSite.getOriginalSignature().asMethodSignature(),
+                            ListUtils.last(outlineMappedRanges).originalRange,
+                            outlineCallSite.getRenamedName()));
+                  }
+                  for (int tailInlineFrameIndex = composedRangeIndex + 1;
+                      tailInlineFrameIndex < composedRanges.size();
+                      tailInlineFrameIndex++) {
+                    MappedRange originalMappedRange = composedRanges.get(tailInlineFrameIndex);
+                    if (!originalMappedRange.minifiedRange.equals(outlineCallSite.minifiedRange)) {
+                      break;
+                    }
+                    MappedRange newMappedRange =
+                        originalMappedRange.withMinifiedRange(newMinifiedRange);
+                    newMappedRange.setAdditionalMappingInformationInternal(
+                        originalMappedRange.getAdditionalMappingInformation());
+                    newComposedRanges.add(newMappedRange);
+                  }
+                  minifiedLine++;
+                }
+                // We have patched up the the inlined outline and all subsequent inline frames
+                // (although some of the subsequent frames above could also be inlined outlines). We
+                // therefore need to copy the remaining frames.
+                boolean seenMinifiedRange = false;
+                for (MappedRange range : composedRanges) {
+                  if (range.minifiedRange.equals(outline.minifiedRange)) {
+                    seenMinifiedRange = true;
+                  } else if (seenMinifiedRange) {
+                    newComposedRanges.add(range);
+                  }
+                }
+                composedRanges = newComposedRanges;
+                outlineCallSitesToRemove.add(outlineCallSiteInformation);
+                outlinesToRemove.add(outline.getOutlineMappingInformation());
+              }
+            }
+            // If we removed any outlines or call site, remove the processing of them.
+            outlineCallSitesToRemove.forEach(
+                computedOutlineInformation.outlineCallsiteMappingInformationToPatchUp::remove);
+            outlinesToRemove.forEach(
+                computedOutlineInformation.seenOutlineMappingInformation::remove);
+          }
+          if (!computedOutlineInformation.seenOutlineMappingInformation.isEmpty()) {
+            MappedRange lastComposedRange = ListUtils.last(composedRanges);
             current
                 .getUpdateOutlineCallsiteInformation(
                     committedPreviousClassBuilder.getRenamedName(),
                     ListUtils.last(newMappedRanges).signature.getName(),
                     lastComposedRange.getRenamedName())
                 .setNewMappedRanges(newMappedRanges);
-            lastComposedRange.addMappingInformation(
-                computedOutlineInformation.seenOutlineMappingInformation,
-                ConsumerUtils.emptyConsumer());
           }
           if (!computedOutlineInformation.outlineCallsiteMappingInformationToPatchUp.isEmpty()) {
+            MappedRange lastComposedRange = ListUtils.last(composedRanges);
+            List<MappedRange> composedRangesFinal = composedRanges;
             // Outline positions are synthetic positions and they have no position in the residual
             // program. We therefore have to find the original positions and copy all inline frames
             // and amend the outermost frame with the residual signature and the next free position.
             List<OutlineCallsiteMappingInformation> outlineCallSites =
                 new ArrayList<>(
-                    computedOutlineInformation.outlineCallsiteMappingInformationToPatchUp);
+                    computedOutlineInformation.outlineCallsiteMappingInformationToPatchUp.keySet());
             outlineCallSites.sort(Comparator.comparing(mapping -> mapping.getOutline().toString()));
-            int firstAvailableRange = lastComposedRange.minifiedRange.to + 1;
+            IntBox firstAvailableRange = new IntBox(lastComposedRange.minifiedRange.to + 1);
             for (OutlineCallsiteMappingInformation outlineCallSite : outlineCallSites) {
-              Int2IntSortedMap positionMap = outlineCallSite.getPositions();
-              MethodSignature originalSignature =
-                  memberNaming.getOriginalSignature().asMethodSignature();
-              ComposingClassBuilder existingClassBuilderForOutline =
-                  getExistingClassBuilder(originalSignature);
-              if (existingClassBuilderForOutline == null) {
-                assert false;
-                continue;
-              }
-              SegmentTree<List<MappedRange>> outlineSegmentTree =
-                  existingClassBuilderForOutline.methodsWithPosition.get(originalSignature);
-              if (outlineSegmentTree == null) {
-                assert false;
-                continue;
-              }
-              Int2IntSortedMap newPositionMap = new Int2IntLinkedOpenHashMap(positionMap.size());
-              for (Integer keyPosition : positionMap.keySet()) {
-                int keyPositionInt = keyPosition;
-                int originalDestination = positionMap.get(keyPositionInt);
-                ExistingMapping existingMapping =
-                    computeExistingMapping(outlineSegmentTree.find(originalDestination));
-                List<MappedRange> mappedRangesForOutlinePosition =
-                    existingMapping.getMappedRangesForPosition(originalDestination);
-                if (mappedRangesForOutlinePosition == null) {
-                  assert false;
-                  continue;
-                }
-                MappedRange outerMostOutlineFrame = ListUtils.last(mappedRangesForOutlinePosition);
-                assert outerMostOutlineFrame.minifiedRange.span() == 1;
-                Range newMinifiedRange = new Range(firstAvailableRange, firstAvailableRange);
-                for (MappedRange inlineMappedRangeInOutlinePosition :
-                    mappedRangesForOutlinePosition) {
-                  if (inlineMappedRangeInOutlinePosition != outerMostOutlineFrame) {
-                    composedRanges.add(
+              Int2IntSortedMap newPositionMap =
+                  new Int2IntLinkedOpenHashMap(outlineCallSite.getPositions().size());
+              visitOutlineMappedPositions(
+                  outlineCallSite,
+                  memberNaming.getOriginalSignature().asMethodSignature(),
+                  (originalPosition, mappedRangesForOutlinePosition) -> {
+                    int newIndex = firstAvailableRange.getAndIncrement();
+                    Range newMinifiedRange = new Range(newIndex, newIndex);
+                    MappedRange outerMostOutlineFrame =
+                        ListUtils.last(mappedRangesForOutlinePosition);
+                    for (MappedRange inlineMappedRangeInOutlinePosition :
+                        mappedRangesForOutlinePosition) {
+                      if (inlineMappedRangeInOutlinePosition != outerMostOutlineFrame) {
+                        composedRangesFinal.add(
+                            inlineMappedRangeInOutlinePosition.withMinifiedRange(newMinifiedRange));
+                      }
+                    }
+                    composedRangesFinal.add(
                         new MappedRange(
                             newMinifiedRange,
-                            inlineMappedRangeInOutlinePosition.signature,
-                            inlineMappedRangeInOutlinePosition.getOriginalRangeOrIdentity(),
-                            inlineMappedRangeInOutlinePosition.getRenamedName()));
-                  }
-                }
-                composedRanges.add(
-                    new MappedRange(
-                        newMinifiedRange,
-                        lastComposedRange.signature,
-                        outerMostOutlineFrame.originalRange,
-                        lastComposedRange.getRenamedName()));
-                newPositionMap.put(keyPositionInt, firstAvailableRange);
-                firstAvailableRange = newMinifiedRange.to + 1;
-              }
-              outlineCallSite.setPositionsInternal(newPositionMap);
+                            lastComposedRange.signature,
+                            outerMostOutlineFrame.originalRange,
+                            lastComposedRange.getRenamedName()));
+                    newPositionMap.put((int) originalPosition, newIndex);
+                    outlineCallSite.setPositionsInternal(newPositionMap);
+                  });
             }
           }
           MethodSignature residualSignature =
               memberNaming
                   .computeResidualSignature(type -> inverseClassMapping.getOrDefault(type, type))
                   .asMethodSignature();
-          if (lastComposedRange.minifiedRange != null) {
+          if (ListUtils.last(composedRanges).minifiedRange != null) {
             SegmentTree<List<MappedRange>> listSegmentTree =
                 methodsWithPosition.computeIfAbsent(
                     residualSignature, ignored -> new SegmentTree<>(false));
@@ -749,12 +867,57 @@
                 minified.getStartOrNoRangeFrom(), minified.getEndOrNoRangeFrom(), composedRanges);
           } else {
             assert composedRanges.size() == 1;
-            methodsWithoutPosition.put(residualSignature, lastComposedRange);
+            methodsWithoutPosition.put(residualSignature, ListUtils.last(composedRanges));
           }
         }
       }
     }
 
+    private void visitOutlineMappedPositions(
+        OutlineCallsiteMappingInformation outlineCallSite,
+        MethodSignature originalSignature,
+        BiConsumer<Integer, List<MappedRange>> outlinePositionConsumer)
+        throws MappingComposeException {
+      Int2IntSortedMap positionMap = outlineCallSite.getPositions();
+      ComposingClassBuilder existingClassBuilder = getExistingClassBuilder(originalSignature);
+      if (existingClassBuilder == null) {
+        throw new MappingComposeException(
+            "Could not find builder with original signature '" + originalSignature + "'.");
+      }
+      SegmentTree<List<MappedRange>> outlineSegmentTree =
+          existingClassBuilder.methodsWithPosition.get(
+              originalSignature.toUnqualifiedSignatureIfQualified().asMethodSignature());
+      if (outlineSegmentTree == null) {
+        throw new MappingComposeException(
+            "Could not find method positions for original signature '" + originalSignature + "'.");
+      }
+      for (Integer keyPosition : positionMap.keySet()) {
+        int keyPositionInt = keyPosition;
+        int originalDestination = positionMap.get(keyPositionInt);
+        List<MappedRange> mappedRanges = outlineSegmentTree.find(originalDestination);
+        if (mappedRanges == null) {
+          throw new MappingComposeException(
+              "Could not find ranges for outline position '"
+                  + keyPosition
+                  + "' with original signature '"
+                  + originalSignature
+                  + "'.");
+        }
+        ExistingMapping existingMapping = computeExistingMapping(mappedRanges);
+        List<MappedRange> mappedRangesForOutlinePosition =
+            existingMapping.getMappedRangesForPosition(originalDestination);
+        if (mappedRangesForOutlinePosition == null) {
+          throw new MappingComposeException(
+              "Could not find ranges for outline position '"
+                  + keyPosition
+                  + "' with original signature '"
+                  + originalSignature
+                  + "'.");
+        }
+        outlinePositionConsumer.accept(keyPositionInt, mappedRangesForOutlinePosition);
+      }
+    }
+
     private void splitOnNewMinifiedRange(
         List<MappedRange> mappedRanges,
         List<MappedRange> previouslyMapped,
@@ -822,6 +985,7 @@
     }
 
     private List<MappedRange> composeMappedRangesForMethod(
+        ComposingClassBuilder existingClassBuilder,
         List<MappedRange> existingRanges,
         MappedRange newRange,
         ComputedOutlineInformation computedOutlineInformation)
@@ -834,7 +998,13 @@
       if (newRange.getOriginalRangeOrIdentity() == null) {
         MappedRange newComposedRange =
             new MappedRange(
-                newRange.minifiedRange, lastExistingRange.signature, null, newRange.renamedName);
+                newRange.minifiedRange,
+                potentiallyQualifySignature(
+                    newRange.signature,
+                    lastExistingRange.signature,
+                    existingClassBuilder.getOriginalName()),
+                null,
+                newRange.renamedName);
         composeMappingInformation(
             newComposedRange.getAdditionalMappingInformation(),
             lastExistingRange.getAdditionalMappingInformation(),
@@ -858,7 +1028,10 @@
           return Collections.singletonList(
               new MappedRange(
                   newRange.minifiedRange,
-                  lastExistingRange.signature,
+                  potentiallyQualifySignature(
+                      newRange.signature,
+                      lastExistingRange.signature,
+                      existingClassBuilder.getOriginalName()),
                   lastExistingRange.originalRange != null
                           && lastExistingRange.originalRange.span() == 1
                       ? lastExistingRange.originalRange
@@ -882,6 +1055,7 @@
       // then we have a perfect mapping that we can translate directly.
       if (lastExistingMappedRange.minifiedRange.equals(newRange.getOriginalRangeOrIdentity())) {
         computeComposedMappedRange(
+            existingClassBuilder,
             newComposedRanges,
             newRange,
             existingMappedRanges,
@@ -907,6 +1081,7 @@
                 lastExistingMappedRangeForPosition.minifiedRange)) {
           // We have seen an existing range we have to compute a splitting for.
           computeComposedMappedRange(
+              existingClassBuilder,
               newComposedRanges,
               newRange,
               existingMappedRanges,
@@ -949,6 +1124,7 @@
         }
       }
       computeComposedMappedRange(
+          existingClassBuilder,
           newComposedRanges,
           newRange,
           existingMappedRanges,
@@ -1038,6 +1214,7 @@
     }
 
     private void computeComposedMappedRange(
+        ComposingClassBuilder existingClassBuilder,
         List<MappedRange> newComposedRanges,
         MappedRange newMappedRange,
         List<MappedRange> existingMappedRanges,
@@ -1071,7 +1248,10 @@
         MappedRange computedRange =
             new MappedRange(
                 newMinifiedRange,
-                existingMappedRange.signature,
+                potentiallyQualifySignature(
+                    newMappedRange.signature,
+                    existingMappedRange.signature,
+                    existingClassBuilder.getOriginalName()),
                 newOriginalRange,
                 newMappedRange.renamedName);
         List<MappingInformation> mappingInformationToCompose = new ArrayList<>();
@@ -1080,13 +1260,17 @@
             .forEach(
                 info -> {
                   if (info.isOutlineMappingInformation()) {
-                    computedOutlineInformation.seenOutlineMappingInformation =
-                        info.asOutlineMappingInformation();
-                    return;
-                  }
-                  if (info.isOutlineCallsiteInformation()) {
-                    computedOutlineInformation.outlineCallsiteMappingInformationToPatchUp.add(
-                        info.asOutlineCallsiteInformation());
+                    computedOutlineInformation
+                        .seenOutlineMappingInformation
+                        .computeIfAbsent(
+                            info.asOutlineMappingInformation(), ignoreArgument(ArrayList::new))
+                        .add(new ComputedMappedRangeForOutline(newMappedRange, computedRange));
+                  } else if (info.isOutlineCallsiteInformation()) {
+                    computedOutlineInformation
+                        .outlineCallsiteMappingInformationToPatchUp
+                        .computeIfAbsent(
+                            info.asOutlineCallsiteInformation(), ignoreArgument(ArrayList::new))
+                        .add(new ComputedMappedRangeForOutline(newMappedRange, computedRange));
                   }
                   mappingInformationToCompose.add(info);
                 });
@@ -1229,6 +1413,14 @@
       }
     }
 
+    private MethodSignature potentiallyQualifySignature(
+        MethodSignature newSignature, MethodSignature signature, String originalHolder) {
+      return !newSignature.isQualified() || signature.isQualified()
+          ? signature
+          : new MethodSignature(
+              originalHolder + "." + signature.name, signature.type, signature.parameters);
+    }
+
     private static class RangeBuilder {
 
       private int start = Integer.MAX_VALUE;
@@ -1256,9 +1448,35 @@
     }
 
     private static class ComputedOutlineInformation {
-      private final Set<OutlineCallsiteMappingInformation>
-          outlineCallsiteMappingInformationToPatchUp = Sets.newIdentityHashSet();
-      private OutlineMappingInformation seenOutlineMappingInformation = null;
+      private final Map<OutlineCallsiteMappingInformation, List<ComputedMappedRangeForOutline>>
+          outlineCallsiteMappingInformationToPatchUp = new IdentityHashMap<>();
+      private final Map<OutlineMappingInformation, List<ComputedMappedRangeForOutline>>
+          seenOutlineMappingInformation = new IdentityHashMap<>();
+
+      private ComputedMappedRangeForOutline getComputedRange(
+          MappingInformation outline, MappedRange current) {
+        List<ComputedMappedRangeForOutline> outlineMappingInformations =
+            outline.isOutlineMappingInformation()
+                ? seenOutlineMappingInformation.get(outline.asOutlineMappingInformation())
+                : outlineCallsiteMappingInformationToPatchUp.get(
+                    outline.asOutlineCallsiteInformation());
+        if (outlineMappingInformations == null) {
+          return null;
+        }
+        return ListUtils.firstMatching(
+            outlineMappingInformations,
+            pair -> pair.composed.minifiedRange.contains(current.minifiedRange.from));
+      }
+    }
+
+    private static class ComputedMappedRangeForOutline {
+      private final MappedRange current;
+      private final MappedRange composed;
+
+      private ComputedMappedRangeForOutline(MappedRange current, MappedRange composed) {
+        this.current = current;
+        this.composed = composed;
+      }
     }
   }
 }
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 d1bfb78..5ddf11e 100644
--- a/src/main/java/com/android/tools/r8/naming/MemberNaming.java
+++ b/src/main/java/com/android/tools/r8/naming/MemberNaming.java
@@ -510,6 +510,10 @@
       return new MethodSignature(toUnqualifiedName(), type, parameters);
     }
 
+    public Signature toUnqualifiedSignatureIfQualified() {
+      return isQualified() ? new MethodSignature(toUnqualifiedName(), type, parameters) : this;
+    }
+
     @Override
     public Signature toQualifiedSignature(String holder) {
       return new MethodSignature(holder + "." + name, type, parameters);
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeInlineFollowedByInlineTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeInlineFollowedByInlineTest.java
index 04ffc15..dbfcdb5 100644
--- a/src/test/java/com/android/tools/r8/mappingcompose/ComposeInlineFollowedByInlineTest.java
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeInlineFollowedByInlineTest.java
@@ -58,9 +58,9 @@
       StringUtils.unixLines(
           "# {'id':'com.android.tools.r8.mapping','version':'experimental'}",
           "com.bar -> c:",
-          "    2:2:void inlinee1():43:43 -> y",
+          "    2:2:void com.foo.inlinee1():43:43 -> y",
           "    2:2:void foo.bar.baz.inlinee1():41 -> y",
-          "    2:2:void caller():40 -> y",
+          "    2:2:void com.foo.caller():40 -> y",
           "    2:2:void inlinee2():42:42 -> y",
           "    2:2:void foo.bar.baz.inlinee1():41 -> y",
           "    2:2:void caller():40 -> y",
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeInlineResidualQualifiedTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeInlineResidualQualifiedTest.java
new file mode 100644
index 0000000..1912d84
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeInlineResidualQualifiedTest.java
@@ -0,0 +1,62 @@
+// Copyright (c) 2023, 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.mappingcompose;
+
+import static com.android.tools.r8.mappingcompose.ComposeTestHelpers.doubleToSingleQuote;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.naming.MappingComposer;
+import com.android.tools.r8.utils.StringUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ComposeInlineResidualQualifiedTest extends TestBase {
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  private static final String mappingFoo =
+      StringUtils.unixLines(
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
+          "com.foo -> A:",
+          "    1:3:void method1():42:44 -> x",
+          "com.bar -> B:",
+          "    4:6:void method2():104:106 -> x");
+  private static final String mappingBar =
+      StringUtils.unixLines(
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
+          "B -> C:",
+          "    1:3:void A.x():2:2 -> y",
+          "    1:3:void x():4 -> y",
+          "    2:3:void x():5:6 -> y");
+  private static final String mappingResult =
+      StringUtils.unixLines(
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
+          "com.bar -> C:",
+          "    1:3:void com.foo.method1():43:43 -> y",
+          "    1:3:void method2():104:104 -> y",
+          "    2:3:void method2():105:106 -> y",
+          "com.foo -> A:");
+
+  @Test
+  public void testCompose() throws Exception {
+    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromString(mappingFoo);
+    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromString(mappingBar);
+    String composed = MappingComposer.compose(mappingForFoo, mappingForBar);
+    assertEquals(mappingResult, doubleToSingleQuote(composed));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeMovedMethodTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeMovedMethodTest.java
index 80c4dec..2991061 100644
--- a/src/test/java/com/android/tools/r8/mappingcompose/ComposeMovedMethodTest.java
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeMovedMethodTest.java
@@ -46,7 +46,7 @@
           "# {'id':'com.android.tools.r8.mapping','version':'experimental'}",
           "com.bar -> b:",
           "    int some.other.Class.f1() -> h1",
-          "    void f2() -> h2",
+          "    void com.foo.f2() -> h2",
           "com.foo -> a:");
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeMovedMethodWithPositionTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeMovedMethodWithPositionTest.java
index 68d2c86..db17b8d 100644
--- a/src/test/java/com/android/tools/r8/mappingcompose/ComposeMovedMethodWithPositionTest.java
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeMovedMethodWithPositionTest.java
@@ -48,7 +48,7 @@
           "com.bar -> b:",
           "    12:14:int some.other.Class.f1():102:104 -> h11",
           "    15:16:int some.other.Class.f1():102:103 -> h12",
-          "    22:23:void f2():114:114 -> h2",
+          "    22:23:void com.foo.f2():114:114 -> h2",
           "com.foo -> a:");
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeOutlineHavingInlineInlinedTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeOutlineHavingInlineInlinedTest.java
new file mode 100644
index 0000000..03e93e6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeOutlineHavingInlineInlinedTest.java
@@ -0,0 +1,73 @@
+// Copyright (c) 2023, 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.mappingcompose;
+
+import static com.android.tools.r8.mappingcompose.ComposeTestHelpers.doubleToSingleQuote;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.naming.MappingComposer;
+import com.android.tools.r8.utils.StringUtils;
+import org.junit.Test;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+public class ComposeOutlineHavingInlineInlinedTest extends TestBase {
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  private static final String mappingFoo =
+      StringUtils.unixLines(
+          "# { id: 'com.android.tools.r8.mapping', version: '2.2' }",
+          "outline.Class -> A:",
+          "    1:2:int some.inlinee():75:76 -> a",
+          "    1:2:int outline():0 -> a",
+          "    # { 'id':'com.android.tools.r8.outline' }",
+          "outline.Callsite -> X:",
+          "    1:2:int outlineCaller(int):41:42 -> s",
+          "    3:3:int outlineCaller(int):0:0 -> s",
+          "    # { 'id':'com.android.tools.r8.outlineCallsite',"
+              + "'positions': { '1': 9, '2': 10 },"
+              + "'outline':'La;a()I' }",
+          "    9:9:int outlineCaller(int):23 -> s",
+          "    10:10:int foo.bar.baz.outlineCaller(int):98:98 -> s",
+          "    10:10:int outlineCaller(int):24 -> s");
+  private static final String mappingBar =
+      StringUtils.unixLines(
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
+          "X -> Y:",
+          "    1:1:int A.a():1:1 -> s",
+          "    1:1:int s(int):3 -> s",
+          "    2:4:int another.inline():102:104 -> s",
+          "    2:4:int A.a():2 -> s",
+          "    2:4:int s(int):3 -> s");
+  private static final String mappingResult =
+      StringUtils.unixLines(
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
+          "outline.Callsite -> Y:",
+          "    1:1:int some.inlinee():75:75 -> s",
+          "    1:1:int outlineCaller(int):23 -> s",
+          "    2:4:int another.inline():102:104 -> s",
+          "    2:4:int some.inlinee():76:76 -> s",
+          "    2:4:int foo.bar.baz.outlineCaller(int):98:98 -> s",
+          "    2:4:int outlineCaller(int):24 -> s",
+          "outline.Class -> A:");
+
+  @Test
+  public void testCompose() throws Exception {
+    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromString(mappingFoo);
+    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromString(mappingBar);
+    String composed = MappingComposer.compose(mappingForFoo, mappingForBar);
+    assertEquals(mappingResult, doubleToSingleQuote(composed));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeOutlineInlineTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeOutlineInlineTest.java
index 9203fed..0c50e79 100644
--- a/src/test/java/com/android/tools/r8/mappingcompose/ComposeOutlineInlineTest.java
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeOutlineInlineTest.java
@@ -62,22 +62,10 @@
           "package.Class -> package.new_internal.Y:",
           "# {'id':'sourceFile','fileName':'FieldDefinition.java'}",
           "    1:6:void foo():21:26 -> b",
-          // TODO(b/284925475): We should put in the inline positions here instead of a reference to
-          //  the outline
-          "    7:8:long package.Int2IntLinkedOpenHashMap$$InternalSyntheticOutline$HASH$0"
-              + ".m(long,long,long):0:1 -> b",
-          // TODO(b/284925475): This is not synthesized.
-          "    # {'id':'com.android.tools.r8.synthesized'}",
-          "    7:8:void foo():0:0 -> b",
-          "    # {'id':'com.android.tools.r8.outlineCallsite',"
-              + "'positions':{'1':11,'2':12},"
-              + "'outline':'Lpackage/internal/X;a(JJJ)J'}",
+          "    7:7:void inlineeInOutline():1337:1337 -> b",
+          "    7:7:void foo():42 -> b",
+          "    8:8:void foo():44:44 -> b",
           "    9:10:void foo():38:39 -> b",
-          // TODO(b/284925475): This is not an outline or outline call site.
-          "    # {'id':'com.android.tools.r8.outline'}",
-          "    11:11:void inlineeInOutline():1337:1337 -> a",
-          "    11:11:void foo():42 -> b",
-          "    12:12:void foo():44:44 -> b",
           "package.Class$$ExternalSyntheticOutline0 -> package.internal.X:",
           "# {'id':'sourceFile','fileName':'R8$$SyntheticClass'}",
           "# {'id':'com.android.tools.r8.synthesized'}");
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeOutlineInlinedIntoOutlineAndInlinedTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeOutlineInlinedIntoOutlineAndInlinedTest.java
new file mode 100644
index 0000000..9dc41ea
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeOutlineInlinedIntoOutlineAndInlinedTest.java
@@ -0,0 +1,84 @@
+// Copyright (c) 2023, 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.mappingcompose;
+
+import static com.android.tools.r8.mappingcompose.ComposeTestHelpers.doubleToSingleQuote;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.naming.MappingComposer;
+import com.android.tools.r8.utils.StringUtils;
+import org.junit.Test;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+public class ComposeOutlineInlinedIntoOutlineAndInlinedTest extends TestBase {
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  private static final String mappingFoo =
+      StringUtils.unixLines(
+          "# { id: 'com.android.tools.r8.mapping', version: '2.2' }",
+          "Outline1 -> A:",
+          "    1:2:int outline1():0:1 -> a",
+          "    # { 'id':'com.android.tools.r8.outline' }",
+          "Outline2 -> B:",
+          "    1:2:int outline2():0:1 -> a",
+          "    # { 'id':'com.android.tools.r8.outline' }",
+          "outline.Callsite1 -> X:",
+          "    1:2:int caller1(int):41:42 -> s",
+          "    3:3:int caller1(int):0:0 -> s",
+          "    # { 'id':'com.android.tools.r8.outlineCallsite',"
+              + "'positions': { '1': 9, '2': 10 },"
+              + "'outline':'La;a()I' }",
+          "    4:6:int caller1(int):48:49 -> s",
+          "    9:9:int caller1(int):23 -> s",
+          "    10:10:int caller1(int):24 -> s",
+          "outline.Callsite2 -> Y:",
+          "    1:1:int caller2(int):0:0 -> s",
+          "    # { 'id':'com.android.tools.r8.outlineCallsite',"
+              + "'positions': { '1': 2, '2': 3 },"
+              + "'outline':'La;a()I' }",
+          "    2:2:int caller2(int):23:23 -> s",
+          "    3:3:int caller2(int):24:24 -> s");
+  private static final String mappingBar =
+      StringUtils.unixLines(
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
+          "Y -> Z:",
+          "    1:2:int A.a():1:2 -> a",
+          "    1:2:int X.s(int):3 -> a",
+          "    1:2:int B.a():1 -> a",
+          "    1:2:int s(int):1 -> a",
+          "    3:3:int B.a():2:2 -> a",
+          "    3:3:int s(int):1 -> a");
+  private static final String mappingResult =
+      StringUtils.unixLines(
+          "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
+          "Outline1 -> A:",
+          "Outline2 -> B:",
+          "outline.Callsite1 -> X:",
+          "outline.Callsite2 -> Z:",
+          "    1:1:int outline.Callsite1.caller1(int):23 -> a",
+          "    1:1:int caller2(int):23:23 -> a",
+          "    2:2:int outline.Callsite1.caller1(int):24 -> a",
+          "    2:2:int caller2(int):23:23 -> a",
+          "    3:3:int caller2(int):24:24 -> a");
+
+  @Test
+  public void testCompose() throws Exception {
+    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromString(mappingFoo);
+    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromString(mappingBar);
+    String composed = MappingComposer.compose(mappingForFoo, mappingForBar);
+    assertEquals(mappingResult, doubleToSingleQuote(composed));
+  }
+}