[Compose] Add support for composing outline call sites

Bug: b/242682464
Change-Id: I476e64fbd2206763782a75b938ae2e380862503d
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 6e1bc00..d99e801 100644
--- a/src/main/java/com/android/tools/r8/naming/ComposingBuilder.java
+++ b/src/main/java/com/android/tools/r8/naming/ComposingBuilder.java
@@ -11,14 +11,22 @@
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.naming.mappinginformation.MapVersionMappingInformation;
 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.naming.mappinginformation.RewriteFrameMappingInformation.ThrowsCondition;
+import com.android.tools.r8.references.ArrayReference;
+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.utils.BiMapContainer;
+import com.android.tools.r8.references.TypeReference;
 import com.android.tools.r8.utils.ChainableStringConsumer;
 import com.android.tools.r8.utils.ConsumerUtils;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.SegmentTree;
+import com.google.common.collect.Sets;
+import it.unimi.dsi.fastutil.ints.Int2IntLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.ints.Int2IntSortedMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
 import java.util.ArrayList;
@@ -28,48 +36,70 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Consumer;
 
 public class ComposingBuilder {
 
-  /**
-   * To ensure we can do alpha renaming of classes and members without polluting the existing
-   * mappping, we use a committed map that we update for each class name mapping. That allows us to
-   * rename to existing renamed names as long as these are also renamed later in the map.
-   */
-  private final Map<String, ComposingClassBuilder> committed = new HashMap<>();
-
-  private Map<String, ComposingClassBuilder> current = new HashMap<>();
-
   private MapVersionMappingInformation currentMapVersion = null;
 
-  private final ComposingSharedData sharedData = new ComposingSharedData();
+  /**
+   * When composing we store a view of the previously known mappings in committed and retain a
+   * current working set. When composing of a new map is finished we commit everything in current
+   * into the committed set.
+   *
+   * <p>The reason for not having just a single set is that we can have a circular mapping as
+   * follows:
+   *
+   * <pre>
+   *   a -> b:
+   *   ...
+   *   b -> a:
+   * </pre>
+   *
+   * After composing our current view of a with the above, we could end up transforming 'a' into 'b'
+   * and then later transforming 'b' back into 'a' again. To ensure we do not mess up namings while
+   * composing classes and methods we resort to a working set and committed set.
+   */
+  private final ComposingData committed = new ComposingData();
+
+  private ComposingData current;
 
   public void compose(ClassNameMapper classNameMapper) throws MappingComposeException {
-    MapVersionMappingInformation thisMapVersion = classNameMapper.getFirstMapVersionInformation();
-    if (thisMapVersion != null) {
+    current = new ComposingData();
+    MapVersionMappingInformation newMapVersionInfo =
+        classNameMapper.getFirstMapVersionInformation();
+    if (newMapVersionInfo != null) {
+      MapVersion newMapVersion = newMapVersionInfo.getMapVersion();
+      if (newMapVersion.isLessThan(MapVersion.MAP_VERSION_2_1) || newMapVersion.isUnknown()) {
+        throw new MappingComposeException(
+            "Composition of mapping files supported from map version 2.1.");
+      }
       if (currentMapVersion == null
-          || currentMapVersion.getMapVersion().isLessThan(thisMapVersion.getMapVersion())) {
-        currentMapVersion = thisMapVersion;
+          || currentMapVersion.getMapVersion().isLessThan(newMapVersion)) {
+        currentMapVersion = newMapVersionInfo;
       }
     }
-    sharedData.patchupMappingInformation(classNameMapper);
     for (ClassNamingForNameMapper classMapping : classNameMapper.getClassNameMappings().values()) {
       compose(classMapping);
     }
-    commit();
+    committed.commit(current, classNameMapper);
   }
 
   private void compose(ClassNamingForNameMapper classMapping) throws MappingComposeException {
     String originalName = classMapping.originalName;
-    ComposingClassBuilder composingClassBuilder = committed.get(originalName);
+    ComposingClassBuilder composingClassBuilder = committed.classBuilders.get(originalName);
     String renamedName = classMapping.renamedName;
     if (composingClassBuilder == null) {
-      composingClassBuilder = new ComposingClassBuilder(originalName, renamedName, sharedData);
+      composingClassBuilder = new ComposingClassBuilder(originalName, renamedName);
     } else {
       composingClassBuilder.setRenamedName(renamedName);
-      committed.remove(originalName);
+      committed.classBuilders.remove(originalName);
     }
-    ComposingClassBuilder duplicateMapping = current.put(renamedName, composingClassBuilder);
+    composingClassBuilder.setCurrentComposingData(current, classMapping.originalName);
+    ComposingClassBuilder duplicateMapping =
+        current.classBuilders.put(renamedName, composingClassBuilder);
     if (duplicateMapping != null) {
       throw new MappingComposeException(
           "Duplicate class mapping. Both '"
@@ -83,31 +113,13 @@
     composingClassBuilder.compose(classMapping);
   }
 
-  private void commit() throws MappingComposeException {
-    for (Entry<String, ComposingClassBuilder> newEntry : current.entrySet()) {
-      String renamedName = newEntry.getKey();
-      ComposingClassBuilder classBuilder = newEntry.getValue();
-      ComposingClassBuilder duplicateMapping = committed.put(renamedName, classBuilder);
-      if (duplicateMapping != null) {
-        throw new MappingComposeException(
-            "Duplicate class mapping. Both '"
-                + duplicateMapping.getOriginalName()
-                + "' and '"
-                + classBuilder.getOriginalName()
-                + "' maps to '"
-                + renamedName
-                + "'.");
-      }
-    }
-    current = new HashMap<>();
-  }
 
   @Override
   public String toString() {
-    List<ComposingClassBuilder> classBuilders = new ArrayList<>(committed.values());
+    List<ComposingClassBuilder> classBuilders = new ArrayList<>(committed.classBuilders.values());
     classBuilders.sort(Comparator.comparing(ComposingClassBuilder::getOriginalName));
     StringBuilder sb = new StringBuilder();
-    // TODO(b/241763080): Keep preamble of mapping files"
+    // TODO(b/241763080): Keep preamble of mapping files
     if (currentMapVersion != null) {
       sb.append("# ").append(currentMapVersion.serialize()).append("\n");
     }
@@ -118,34 +130,243 @@
     return sb.toString();
   }
 
-  public static class ComposingSharedData {
+  public static class ComposingData {
 
     /**
+     * A map of minified names to their class builders. When committing to a new minified name we
+     * destructively remove the previous minified mapping and replace it with the up-to-date one.
+     */
+    private final Map<String, ComposingClassBuilder> classBuilders = new HashMap<>();
+    /**
      * RewriteFrameInformation contains condition clauses that are bound to the residual program. As
      * a result of that, we have to patch up the conditions when we compose new class mappings.
      */
-    private final List<RewriteFrameMappingInformation> mappingInformationToPatchUp =
-        new ArrayList<>();
+    private final List<RewriteFrameMappingInformation> rewriteFrameInformation = new ArrayList<>();
+    /** Map of newly added outline call site informations which do not require any rewriting. */
+    private Map<ClassDescriptorAndMethodName, OutlineCallsiteMappingInformation>
+        outlineCallsiteInformation = new HashMap<>();
+    /**
+     * Map of updated outline definitions which has to be committed. The positions in the caller are
+     * fixed at this point since these are local to the method when rewriting.
+     */
+    private final Map<ClassDescriptorAndMethodName, UpdateOutlineCallsiteInformation>
+        outlineSourcePositionsUpdated = new HashMap<>();
 
-    private void patchupMappingInformation(ClassNameMapper classNameMapper) {
-      BiMapContainer<String, String> obfuscatedToOriginalMapping =
-          classNameMapper.getObfuscatedToOriginalMapping();
-      for (RewriteFrameMappingInformation rewriteMappingInfo : mappingInformationToPatchUp) {
+    public void commit(ComposingData current, ClassNameMapper classNameMapper)
+        throws MappingComposeException {
+      commitClassBuilders(current);
+      commitRewriteFrameInformation(current, classNameMapper);
+      commitOutlineCallsiteInformation(current, classNameMapper);
+    }
+
+    private void commitClassBuilders(ComposingData current) throws MappingComposeException {
+      for (Entry<String, ComposingClassBuilder> newEntry : current.classBuilders.entrySet()) {
+        String renamedName = newEntry.getKey();
+        ComposingClassBuilder classBuilder = newEntry.getValue();
+        ComposingClassBuilder duplicateMapping = classBuilders.put(renamedName, classBuilder);
+        if (duplicateMapping != null) {
+          throw new MappingComposeException(
+              "Duplicate class mapping. Both '"
+                  + duplicateMapping.getOriginalName()
+                  + "' and '"
+                  + classBuilder.getOriginalName()
+                  + "' maps to '"
+                  + renamedName
+                  + "'.");
+        }
+      }
+    }
+
+    private void commitRewriteFrameInformation(
+        ComposingData current, ClassNameMapper classNameMapper) {
+      // First update the existing frame information to have new class name mappings.
+      Map<String, String> inverse = classNameMapper.getObfuscatedToOriginalMapping().inverse;
+      for (RewriteFrameMappingInformation rewriteMappingInfo : rewriteFrameInformation) {
         rewriteMappingInfo
             .getConditions()
             .forEach(
                 rewriteCondition -> {
                   ThrowsCondition throwsCondition = rewriteCondition.asThrowsCondition();
                   if (throwsCondition != null) {
-                    String originalName = throwsCondition.getClassReference().getTypeName();
-                    String obfuscatedName = obfuscatedToOriginalMapping.inverse.get(originalName);
-                    if (obfuscatedName != null) {
-                      throwsCondition.setClassReferenceInternal(
-                          Reference.classFromTypeName(obfuscatedName));
-                    }
+                    throwsCondition.setClassReferenceInternal(
+                        mapTypeReference(inverse, throwsCondition.getClassReference()).asClass());
                   }
                 });
       }
+      rewriteFrameInformation.addAll(current.rewriteFrameInformation);
+    }
+
+    private void commitOutlineCallsiteInformation(
+        ComposingData current, ClassNameMapper classNameMapper) {
+      // To commit outline call site information, we take the previously committed and bring forward
+      // to a new mapping, and potentially rewrite source positions if available.
+      Map<ClassDescriptorAndMethodName, OutlineCallsiteMappingInformation> newOutlineCallsiteInfo =
+          new HashMap<>();
+      Map<String, String> inverse = classNameMapper.getObfuscatedToOriginalMapping().inverse;
+      outlineCallsiteInformation.forEach(
+          (holderAndMethodNameOfOutline, outlineInfo) -> {
+            UpdateOutlineCallsiteInformation updateOutlineCallsiteInformation =
+                current.outlineSourcePositionsUpdated.get(holderAndMethodNameOfOutline);
+            String newMethodName = outlineInfo.getOutline().getMethodName();
+            if (updateOutlineCallsiteInformation != null) {
+              // We have a callsite mapping that we need to update.
+              MappedRangeOriginalToMinifiedMap originalToMinifiedMap =
+                  MappedRangeOriginalToMinifiedMap.build(
+                      updateOutlineCallsiteInformation.newMappedRanges);
+              Int2IntSortedMap newPositionMap = new Int2IntLinkedOpenHashMap();
+              outlineInfo
+                  .getPositions()
+                  .forEach(
+                      (originalPosition, destination) -> {
+                        originalToMinifiedMap.visitMinified(
+                            originalPosition,
+                            newMinified -> {
+                              newPositionMap.put(newMinified, destination);
+                            });
+                      });
+              outlineInfo.setPositionsInternal(newPositionMap);
+              newMethodName = updateOutlineCallsiteInformation.newMethodName;
+            }
+            // Holder, return type or formals could have changed the outline descriptor.
+            MethodReference outline = outlineInfo.getOutline();
+            ClassReference newHolder =
+                mapTypeReference(inverse, outline.getHolderClass()).asClass();
+            outlineInfo.setOutlineInternal(
+                Reference.method(
+                    newHolder,
+                    newMethodName,
+                    mapTypeReferences(inverse, outline.getFormalTypes()),
+                    mapTypeReference(inverse, outline.getReturnType())));
+            newOutlineCallsiteInfo.put(
+                new ClassDescriptorAndMethodName(
+                    newHolder.getTypeName(), holderAndMethodNameOfOutline.getMethodName()),
+                outlineInfo);
+          });
+      newOutlineCallsiteInfo.putAll(current.outlineCallsiteInformation);
+      outlineCallsiteInformation = newOutlineCallsiteInfo;
+    }
+
+    public void addNewOutlineCallsiteInformation(
+        MethodReference outline, OutlineCallsiteMappingInformation outlineCallsiteInfo) {
+      outlineCallsiteInformation.put(
+          new ClassDescriptorAndMethodName(
+              outline.getHolderClass().getTypeName(), outline.getMethodName()),
+          outlineCallsiteInfo);
+    }
+
+    public UpdateOutlineCallsiteInformation getUpdateOutlineCallsiteInformation(
+        String originalHolder, String originalMethodName, String newMethodName) {
+      return outlineSourcePositionsUpdated.computeIfAbsent(
+          new ClassDescriptorAndMethodName(originalHolder, originalMethodName),
+          ignore -> new UpdateOutlineCallsiteInformation(newMethodName));
+    }
+
+    private List<TypeReference> mapTypeReferences(
+        Map<String, String> typeNameMap, List<TypeReference> typeReferences) {
+      return ListUtils.map(typeReferences, typeRef -> mapTypeReference(typeNameMap, typeRef));
+    }
+
+    private TypeReference mapTypeReference(
+        Map<String, String> typeNameMap, TypeReference typeReference) {
+      if (typeReference == null || typeReference.isPrimitive()) {
+        return typeReference;
+      }
+      if (typeReference.isArray()) {
+        ArrayReference arrayReference = typeReference.asArray();
+        return Reference.array(
+            mapTypeReference(typeNameMap, arrayReference.getBaseType()),
+            arrayReference.getDimensions());
+      } else {
+        assert typeReference.isClass();
+        String newTypeName = typeNameMap.get(typeReference.getTypeName());
+        return newTypeName == null ? typeReference : Reference.classFromTypeName(newTypeName);
+      }
+    }
+  }
+
+  private static class ClassDescriptorAndMethodName {
+
+    private final String holderTypeName;
+    private final String methodName;
+
+    public ClassDescriptorAndMethodName(String holderTypeName, String methodName) {
+      this.holderTypeName = holderTypeName;
+      this.methodName = methodName;
+    }
+
+    public String getHolderTypeName() {
+      return holderTypeName;
+    }
+
+    public String getMethodName() {
+      return methodName;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof ClassDescriptorAndMethodName)) {
+        return false;
+      }
+      ClassDescriptorAndMethodName that = (ClassDescriptorAndMethodName) o;
+      return holderTypeName.equals(that.holderTypeName) && methodName.equals(that.methodName);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(holderTypeName, methodName);
+    }
+  }
+
+  private static class UpdateOutlineCallsiteInformation {
+
+    private List<MappedRange> newMappedRanges;
+    private final String newMethodName;
+
+    private UpdateOutlineCallsiteInformation(String newMethodName) {
+      this.newMethodName = newMethodName;
+    }
+
+    private void setNewMappedRanges(List<MappedRange> mappedRanges) {
+      newMappedRanges = mappedRanges;
+    }
+  }
+
+  private static class MappedRangeOriginalToMinifiedMap {
+
+    private final Int2ReferenceMap<List<Integer>> originalToMinified;
+
+    private MappedRangeOriginalToMinifiedMap(Int2ReferenceMap<List<Integer>> originalToMinified) {
+      this.originalToMinified = originalToMinified;
+    }
+
+    private static MappedRangeOriginalToMinifiedMap build(List<MappedRange> mappedRanges) {
+      Int2ReferenceMap<List<Integer>> positionMap = new Int2ReferenceOpenHashMap<>();
+      for (MappedRange mappedRange : mappedRanges) {
+        Range originalRange = mappedRange.originalRange;
+        for (int position = originalRange.from; position <= originalRange.to; position++) {
+          // It is perfectly fine to have multiple minified ranges mapping to the same source, we
+          // just need to keep the additional information.
+          positionMap
+              .computeIfAbsent(position, ignoreArgument(ArrayList::new))
+              .add(mappedRange.minifiedRange.from + (position - originalRange.from));
+        }
+      }
+      return new MappedRangeOriginalToMinifiedMap(positionMap);
+    }
+
+    public int lookupFirst(int originalPosition) {
+      List<Integer> minifiedPositions = originalToMinified.get(originalPosition);
+      return minifiedPositions == null ? 0 : minifiedPositions.get(0);
+    }
+
+    public void visitMinified(int originalPosition, Consumer<Integer> consumer) {
+      List<Integer> minifiedPositions = originalToMinified.get(originalPosition);
+      if (minifiedPositions != null) {
+        minifiedPositions.forEach(consumer);
+      }
     }
   }
 
@@ -163,13 +384,23 @@
     // one method since any shrinker should put in line numbers for overloads.
     private final Map<String, SegmentTree<List<MappedRange>>> methodMembers = new HashMap<>();
     private List<MappingInformation> additionalMappingInfo = null;
-    private final ComposingSharedData sharedData;
 
-    private ComposingClassBuilder(
-        String originalName, String renamedName, ComposingSharedData sharedData) {
+    private ComposingData current;
+
+    /**
+     * Keeps track of the current original name which is different from originalName if this is a
+     * subsequent mapping.
+     */
+    private String currentOriginalName;
+
+    private ComposingClassBuilder(String originalName, String renamedName) {
       this.originalName = originalName;
       this.renamedName = renamedName;
-      this.sharedData = sharedData;
+    }
+
+    public void setCurrentComposingData(ComposingData current, String currentMinifiedName) {
+      this.current = current;
+      this.currentOriginalName = currentMinifiedName;
     }
 
     public void setRenamedName(String renamedName) {
@@ -268,7 +499,8 @@
      * long as the current mapped range is the same method and return a mapped range result
      * containing all ranges for a method along with some additional information.
      */
-    private MappedRangeResult getMappedRangesForMethod(List<MappedRange> mappedRanges, int index) {
+    private MappedRangeResult getMappedRangesForMethod(List<MappedRange> mappedRanges, int index)
+        throws MappingComposeException {
       if (index >= mappedRanges.size()) {
         return null;
       }
@@ -297,10 +529,30 @@
             && !isInlineMappedRange(mappedRanges, i)) {
           break;
         }
+        // Register mapping information that is dependent on the residual naming to allow updating
+        // later on.
         for (MappingInformation mappingInformation : thisMappedRange.getAdditionalMappingInfo()) {
           if (mappingInformation.isRewriteFrameMappingInformation()) {
-            sharedData.mappingInformationToPatchUp.add(
-                mappingInformation.asRewriteFrameMappingInformation());
+            RewriteFrameMappingInformation rewriteFrameMappingInformation =
+                mappingInformation.asRewriteFrameMappingInformation();
+            rewriteFrameMappingInformation
+                .getConditions()
+                .forEach(
+                    condition -> {
+                      if (condition.isThrowsCondition()) {
+                        current.rewriteFrameInformation.add(rewriteFrameMappingInformation);
+                      }
+                    });
+          } else if (mappingInformation.isOutlineCallsiteInformation()) {
+            OutlineCallsiteMappingInformation outlineCallsiteInfo =
+                mappingInformation.asOutlineCallsiteInformation();
+            MethodReference outline = outlineCallsiteInfo.getOutline();
+            if (outline == null) {
+              throw new MappingComposeException(
+                  "Unable to compose outline call site information without outline key: "
+                      + outlineCallsiteInfo.serialize());
+            }
+            current.addNewOutlineCallsiteInformation(outline, outlineCallsiteInfo);
           }
         }
         seenMappedRanges.add(thisMappedRange);
@@ -345,6 +597,7 @@
       Int2ReferenceMap<List<MappedRange>> mappedRangesForPosition =
           getExistingMapping(existingRanges);
       List<MappedRange> newComposedRanges = new ArrayList<>();
+      ComputedOutlineInformation computedOutlineInformation = new ComputedOutlineInformation();
       for (int i = 0; i < newRanges.size(); i++) {
         if (isInlineMappedRange(newRanges, i)) {
           throw new MappingComposeException(
@@ -378,6 +631,7 @@
                 newComposedRanges,
                 newRange,
                 existingMappedRanges,
+                computedOutlineInformation,
                 newRange.minifiedRange.from,
                 newRange.minifiedRange.to);
           } else {
@@ -403,6 +657,7 @@
                     newComposedRanges,
                     newRange,
                     existingMappedRanges,
+                    computedOutlineInformation,
                     lastStartingMinifiedFrom,
                     position - 1);
                 lastStartingMinifiedFrom = position;
@@ -413,11 +668,41 @@
                 newComposedRanges,
                 newRange,
                 existingMappedRanges,
+                computedOutlineInformation,
                 lastStartingMinifiedFrom,
                 newRange.minifiedRange.to);
           }
         }
       }
+      MappedRange lastComposedRange = ListUtils.last(newComposedRanges);
+      if (computedOutlineInformation.seenOutlineMappingInformation != null) {
+        current
+            .getUpdateOutlineCallsiteInformation(
+                currentOriginalName,
+                ListUtils.last(newRanges).signature.getName(),
+                lastComposedRange.renamedName)
+            .setNewMappedRanges(newRanges);
+        lastComposedRange.addMappingInformation(
+            computedOutlineInformation.seenOutlineMappingInformation,
+            ConsumerUtils.emptyConsumer());
+      }
+      if (!computedOutlineInformation.outlineCallsiteMappingInformationToPatchUp.isEmpty()) {
+        MappedRangeOriginalToMinifiedMap originalToMinifiedMap =
+            MappedRangeOriginalToMinifiedMap.build(newRanges);
+        List<OutlineCallsiteMappingInformation> outlineCallSites =
+            new ArrayList<>(computedOutlineInformation.outlineCallsiteMappingInformationToPatchUp);
+        outlineCallSites.sort(Comparator.comparing(mapping -> mapping.getOutline().toString()));
+        for (OutlineCallsiteMappingInformation outlineCallSite : outlineCallSites) {
+          Int2IntSortedMap positionMap = outlineCallSite.getPositions();
+          for (Integer keyPosition : positionMap.keySet()) {
+            int keyPositionInt = keyPosition;
+            int originalDestination = positionMap.get(keyPositionInt);
+            int newDestination = originalToMinifiedMap.lookupFirst(originalDestination);
+            positionMap.put(keyPositionInt, newDestination);
+          }
+          lastComposedRange.addMappingInformation(outlineCallSite, ConsumerUtils.emptyConsumer());
+        }
+      }
       return newComposedRanges;
     }
 
@@ -453,6 +738,7 @@
         List<MappedRange> newComposedRanges,
         MappedRange newMappedRange,
         List<MappedRange> existingMappedRanges,
+        ComputedOutlineInformation computedOutlineInformation,
         int lastStartingMinifiedFrom,
         int position) {
       Range existingRange = existingMappedRanges.get(0).minifiedRange;
@@ -485,7 +771,17 @@
         existingMappedRange
             .getAdditionalMappingInfo()
             .forEach(
-                info -> computedRange.addMappingInformation(info, ConsumerUtils.emptyConsumer()));
+                info -> {
+                  if (info.isOutlineMappingInformation()) {
+                    computedOutlineInformation.seenOutlineMappingInformation =
+                        info.asOutlineMappingInformation();
+                  } else if (info.isOutlineCallsiteInformation()) {
+                    computedOutlineInformation.outlineCallsiteMappingInformationToPatchUp.add(
+                        info.asOutlineCallsiteInformation());
+                  } else {
+                    computedRange.addMappingInformation(info, ConsumerUtils.emptyConsumer());
+                  }
+                });
         newComposedRanges.add(computedRange);
       }
     }
@@ -573,5 +869,11 @@
         this.allRanges = allRanges;
       }
     }
+
+    private static class ComputedOutlineInformation {
+      private final Set<OutlineCallsiteMappingInformation>
+          outlineCallsiteMappingInformationToPatchUp = Sets.newIdentityHashSet();
+      private OutlineMappingInformation seenOutlineMappingInformation = null;
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/naming/mappinginformation/MappingInformation.java b/src/main/java/com/android/tools/r8/naming/mappinginformation/MappingInformation.java
index 787a0bc..c7d53f9 100644
--- a/src/main/java/com/android/tools/r8/naming/mappinginformation/MappingInformation.java
+++ b/src/main/java/com/android/tools/r8/naming/mappinginformation/MappingInformation.java
@@ -66,6 +66,10 @@
     return null;
   }
 
+  public OutlineMappingInformation asOutlineMappingInformation() {
+    return null;
+  }
+
   public OutlineCallsiteMappingInformation asOutlineCallsiteInformation() {
     return null;
   }
diff --git a/src/main/java/com/android/tools/r8/naming/mappinginformation/OutlineCallsiteMappingInformation.java b/src/main/java/com/android/tools/r8/naming/mappinginformation/OutlineCallsiteMappingInformation.java
index 5af9615..efdb788 100644
--- a/src/main/java/com/android/tools/r8/naming/mappinginformation/OutlineCallsiteMappingInformation.java
+++ b/src/main/java/com/android/tools/r8/naming/mappinginformation/OutlineCallsiteMappingInformation.java
@@ -24,8 +24,8 @@
   private static final String POSITIONS_KEY = "positions";
   private static final String OUTLINE_KEY = "outline";
 
-  private final Int2IntSortedMap positions;
-  private final MethodReference outline;
+  private Int2IntSortedMap positions;
+  private MethodReference outline;
 
   private OutlineCallsiteMappingInformation(Int2IntSortedMap positions, MethodReference outline) {
     this.positions = positions;
@@ -72,6 +72,10 @@
     return positions.getOrDefault(originalPosition, originalPosition);
   }
 
+  public MethodReference getOutline() {
+    return outline;
+  }
+
   public static OutlineCallsiteMappingInformation create(
       Int2IntSortedMap positions, MethodReference outline) {
     return new OutlineCallsiteMappingInformation(positions, outline);
@@ -112,4 +116,16 @@
       onMappingInfo.accept(OutlineCallsiteMappingInformation.create(positionsMap, outline));
     }
   }
+
+  public void setOutlineInternal(MethodReference outline) {
+    this.outline = outline;
+  }
+
+  public Int2IntSortedMap getPositions() {
+    return positions;
+  }
+
+  public void setPositionsInternal(Int2IntSortedMap positions) {
+    this.positions = positions;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/naming/mappinginformation/OutlineMappingInformation.java b/src/main/java/com/android/tools/r8/naming/mappinginformation/OutlineMappingInformation.java
index 1914431..54c392b 100644
--- a/src/main/java/com/android/tools/r8/naming/mappinginformation/OutlineMappingInformation.java
+++ b/src/main/java/com/android/tools/r8/naming/mappinginformation/OutlineMappingInformation.java
@@ -27,6 +27,11 @@
   }
 
   @Override
+  public OutlineMappingInformation asOutlineMappingInformation() {
+    return this;
+  }
+
+  @Override
   public boolean allowOther(MappingInformation information) {
     return true;
   }
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeMapVersionTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeMapVersionTest.java
index bbc6c01..c91527d 100644
--- a/src/test/java/com/android/tools/r8/mappingcompose/ComposeMapVersionTest.java
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeMapVersionTest.java
@@ -4,13 +4,14 @@
 
 package com.android.tools.r8.mappingcompose;
 
-import static com.android.tools.r8.mappingcompose.ComposeHelpers.doubleToSingleQuote;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
 
 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.MappingComposeException;
 import com.android.tools.r8.naming.MappingComposer;
 import com.android.tools.r8.utils.StringUtils;
 import org.junit.Test;
@@ -31,18 +32,20 @@
 
   private static final String mappingFoo =
       StringUtils.unixLines(
-          "# { id: 'com.android.tools.r8.mapping', version: '1.0' }", "com.foo -> a:");
+          "# { id: 'com.android.tools.r8.mapping', version: '2.1' }", "com.foo -> a:");
   private static final String mappingBar =
-      StringUtils.unixLines("# { id: 'com.android.tools.r8.mapping', version: '2.0' }", "a -> b:");
-  private static final String mappingResult =
-      StringUtils.unixLines(
-          "# {'id':'com.android.tools.r8.mapping','version':'2.0'}", "com.foo -> b:");
+      StringUtils.unixLines("# { id: 'com.android.tools.r8.mapping', version: '2.2' }", "a -> b:");
 
   @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));
+    MappingComposeException mappingComposeException =
+        assertThrows(
+            MappingComposeException.class,
+            () -> MappingComposer.compose(mappingForFoo, mappingForBar));
+    assertEquals(
+        "Composition of mapping files supported from map version 2.1.",
+        mappingComposeException.getMessage());
   }
 }
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeOutlineTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeOutlineTest.java
index 98f1cd5..8ba1011 100644
--- a/src/test/java/com/android/tools/r8/mappingcompose/ComposeOutlineTest.java
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeOutlineTest.java
@@ -5,7 +5,7 @@
 package com.android.tools.r8.mappingcompose;
 
 import static com.android.tools.r8.mappingcompose.ComposeHelpers.doubleToSingleQuote;
-import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -26,12 +26,12 @@
 
   @Parameters(name = "{0}")
   public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimesAndApiLevels().build();
+    return getTestParameters().withNoneRuntime().build();
   }
 
   private static final String mappingFoo =
       StringUtils.unixLines(
-          "# { id: 'com.android.tools.r8.mapping', version: '2.0' }",
+          "# { id: 'com.android.tools.r8.mapping', version: '2.1' }",
           "outline.Class -> a:",
           "    1:2:int some.inlinee():75:76 -> a",
           "    1:2:int outline():0 -> a",
@@ -41,24 +41,27 @@
           "    5:5:int foo.bar.baz.outlineCaller(int):98:98 -> s",
           "    5:5:int outlineCaller(int):24 -> s",
           "    27:27:int outlineCaller(int):0:0 -> s",
-          "    # { 'id':'com.android.tools.r8.outlineCallsite', 'positions': { '1': 4, '2': 5 } }");
-  private static final String mappingBar =
+          "    # { 'id':'com.android.tools.r8.outlineCallsite', 'positions': { '1': 4, '2': 5 },"
+              + " 'outline':'La;a()I' }");
+  private static final String mappingBar = StringUtils.unixLines("a -> b:");
+  private static final String mappingBaz =
       StringUtils.unixLines(
-          "a -> b:",
+          "b -> c:",
           "    4:5:int a():1:2 -> m",
-          "x -> c:",
+          "x -> y:",
           "    8:9:int s(int):4:5 -> o",
           "    42:42:int s(int):27:27 -> o");
   private static final String mappingResult =
       StringUtils.unixLines(
-          "# {'id':'com.android.tools.r8.mapping','version':'2.0'}",
-          "outline.Callsite -> c:",
+          "# {'id':'com.android.tools.r8.mapping','version':'2.1'}",
+          "outline.Callsite -> y:",
           "    8:8:int outlineCaller(int):23 -> o",
           "    9:9:int foo.bar.baz.outlineCaller(int):98:98 -> o",
           "    9:9:int outlineCaller(int):24 -> o",
           "    42:42:int outlineCaller(int):0:0 -> o",
-          "    # { 'id':'com.android.tools.r8.outlineCallsite', 'positions': { '4': 8, '5': 9 } }",
-          "outline.Class -> b:",
+          "    #"
+              + " {'id':'com.android.tools.r8.outlineCallsite','positions':{'4':8,'5':9},'outline':'Lc;m()I'}",
+          "outline.Class -> c:",
           "    4:5:int some.inlinee():75:76 -> m",
           "    4:5:int outline():0 -> m",
           "    # {'id':'com.android.tools.r8.outline'}");
@@ -67,8 +70,8 @@
   public void testCompose() throws Exception {
     ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromString(mappingFoo);
     ClassNameMapper mappingForBar = ClassNameMapper.mapperFromString(mappingBar);
-    String composed = MappingComposer.compose(mappingForFoo, mappingForBar);
-    // TODO(b/242682464): Update this test when the link has been added to the mapping information.
-    assertNotEquals(mappingResult, doubleToSingleQuote(composed));
+    ClassNameMapper mappingForBaz = ClassNameMapper.mapperFromString(mappingBaz);
+    String composed = MappingComposer.compose(mappingForFoo, mappingForBar, mappingForBaz);
+    assertEquals(mappingResult, doubleToSingleQuote(composed));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeRewriteFrameTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeRewriteFrameTest.java
index cae1f89..f8b6ebc 100644
--- a/src/test/java/com/android/tools/r8/mappingcompose/ComposeRewriteFrameTest.java
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeRewriteFrameTest.java
@@ -31,7 +31,7 @@
 
   private static final String mappingFoo =
       StringUtils.unixLines(
-          "# { id: 'com.android.tools.r8.mapping', version: '2.0' }",
+          "# { id: 'com.android.tools.r8.mapping', version: '2.1' }",
           "my.CustomException -> a:",
           "foo.Bar -> x:",
           "    4:4:void other.Class.inlinee():23:23 -> a",
@@ -40,13 +40,13 @@
               + "conditions: ['throws(La;)'], actions: ['removeInnerFrames(1)'] }");
   private static final String mappingBar =
       StringUtils.unixLines(
-          "# { id: 'com.android.tools.r8.mapping', version: '2.0' }",
+          "# { id: 'com.android.tools.r8.mapping', version: '2.1' }",
           "a -> b:",
           "x -> c:",
           "    8:8:void a(Other.Class):4:4 -> m");
   private static final String mappingResult =
       StringUtils.unixLines(
-          "# {'id':'com.android.tools.r8.mapping','version':'2.0'}",
+          "# {'id':'com.android.tools.r8.mapping','version':'2.1'}",
           "foo.Bar -> c:",
           "    8:8:void other.Class.inlinee():23:23 -> m",
           "    8:8:void caller(other.Class):7 -> m",
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeSyntheticTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeSyntheticTest.java
index 20585e9..e91eed6 100644
--- a/src/test/java/com/android/tools/r8/mappingcompose/ComposeSyntheticTest.java
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeSyntheticTest.java
@@ -31,7 +31,7 @@
 
   private static final String mappingFoo =
       StringUtils.unixLines(
-          "# { id: 'com.android.tools.r8.mapping', version: '1.0' }",
+          "# { id: 'com.android.tools.r8.mapping', version: '2.1' }",
           "com.foo -> a:",
           "# { id: 'com.android.tools.r8.synthesized' }",
           "    int f -> a",
@@ -40,7 +40,7 @@
           "    # { id: 'com.android.tools.r8.synthesized' }");
   private static final String mappingBar =
       StringUtils.unixLines(
-          "# { id: 'com.android.tools.r8.mapping', version: '1.0' }",
+          "# { id: 'com.android.tools.r8.mapping', version: '2.1' }",
           "a -> b:",
           "    int a -> b",
           "com.bar -> c:",
@@ -49,7 +49,7 @@
           "    # { id: 'com.android.tools.r8.synthesized' }");
   private static final String mappingResult =
       StringUtils.unixLines(
-          "# {'id':'com.android.tools.r8.mapping','version':'1.0'}",
+          "# {'id':'com.android.tools.r8.mapping','version':'2.1'}",
           "com.bar -> c:",
           "# {'id':'com.android.tools.r8.synthesized'}",
           "    void bar() -> a",