[MappingCompose] Extend mapping composer to account for methods

Bug: b/241763080
Change-Id: If11f95c9edf56ce5832be46626e4ec3ce4cfd028
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 f9266b0..050b1ff 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNamingForNameMapper.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNamingForNameMapper.java
@@ -504,7 +504,7 @@
 
     private List<MappingInformation> additionalMappingInfo = EMPTY_MAPPING_INFORMATION;
 
-    private MappedRange(
+    MappedRange(
         Range minifiedRange, MethodSignature signature, Range originalRange, String renamedName) {
       this.minifiedRange = minifiedRange;
       this.signature = signature;
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 badc6fe..2fcbd3d 100644
--- a/src/main/java/com/android/tools/r8/naming/ComposingBuilder.java
+++ b/src/main/java/com/android/tools/r8/naming/ComposingBuilder.java
@@ -6,12 +6,21 @@
 
 import static com.android.tools.r8.utils.FunctionUtils.ignoreArgument;
 
+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.utils.ChainableStringConsumer;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.SegmentTree;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 
 public class ComposingBuilder {
 
@@ -56,10 +65,16 @@
   public static class ComposingClassBuilder {
 
     private static final String INDENTATION = "    ";
+    private static final int NO_RANGE_FROM = -1;
 
     private final String originalName;
     private String renamedName;
     private final Map<String, List<MemberNaming>> fieldMembers = new HashMap<>();
+    // Ideally we would have liked to use the signature as a key since this uniquely specifies a
+    // method. However, because a mapping file has no right hand side, we therefore assume that a
+    // starting position uniquely identifies a method. If no position is given there can be only
+    // one method since any shrinker should put in line numbers for overloads.
+    private final Map<String, SegmentTree<List<MappedRange>>> methodMembers = new HashMap<>();
 
     private ComposingClassBuilder(String originalName, String renamedName) {
       this.originalName = originalName;
@@ -79,6 +94,12 @@
     }
 
     public void compose(ClassNamingForNameMapper mapper) throws MappingComposeException {
+      composeFieldNamings(mapper);
+      composeMethodNamings(mapper);
+    }
+
+    private void composeFieldNamings(ClassNamingForNameMapper mapper)
+        throws MappingComposeException {
       mapper.forAllFieldNaming(
           fieldNaming -> {
             List<MemberNaming> memberNamings = fieldMembers.get(fieldNaming.getOriginalName());
@@ -89,8 +110,8 @@
               return;
             }
             // There is no right-hand side of field mappings thus if we have seen an existing
-            // mapping we cannot compose the type. For fields we check that the original type is the
-            // same or we throw an error since we cannot guarantee a proper composition.
+            // mapping we cannot compose the type. For fields we check that the original type is
+            // the same or we throw an error since we cannot guarantee a proper composition.
             for (int i = 0; i < memberNamings.size(); i++) {
               MemberNaming memberNaming = memberNamings.get(i);
               assert memberNaming.getRenamedName().equals(fieldNaming.getOriginalName());
@@ -109,14 +130,274 @@
           });
     }
 
+    private void composeMethodNamings(ClassNamingForNameMapper mapper)
+        throws MappingComposeException {
+      for (Entry<String, MappedRangesOfName> entry : mapper.mappedRangesByRenamedName.entrySet()) {
+        MappedRangesOfName value = entry.getValue();
+        List<MappedRange> mappedRanges = value.getMappedRanges();
+        MappedRangeResult mappedRangeResult;
+        int index = 0;
+        while ((mappedRangeResult = getMappedRangesForMethod(mappedRanges, index)) != null) {
+          index = mappedRangeResult.endIndex;
+          MappedRange newMappedRange = mappedRangeResult.lastRange;
+          String originalName = newMappedRange.signature.getName();
+          SegmentTree<List<MappedRange>> listSegmentTree = methodMembers.get(originalName);
+          List<MappedRange> existingMappedRanges = null;
+          if (listSegmentTree != null) {
+            Entry<Integer, List<MappedRange>> existingEntry =
+                listSegmentTree.findEntry(mappedRangeResult.startOriginalPosition);
+            // We assume that all new minified ranges for a method are rewritten in the new map
+            // such that no previous existing positions exists.
+            if (existingEntry != null) {
+              listSegmentTree.removeSegment(existingEntry.getKey());
+              existingMappedRanges = existingEntry.getValue();
+            }
+          }
+          Range minifiedRange = mappedRangeResult.lastRange.minifiedRange;
+          int endMinifiedPosition = minifiedRange == null ? NO_RANGE_FROM : minifiedRange.to;
+          methodMembers
+              .computeIfAbsent(newMappedRange.renamedName, ignored -> new SegmentTree<>(false))
+              .add(
+                  mappedRangeResult.startMinifiedPosition,
+                  endMinifiedPosition,
+                  composeMappedRangesForMethod(existingMappedRanges, mappedRangeResult.allRanges));
+        }
+      }
+    }
+
+    /***
+     * Iterates over mapped ranges in order, starting from index, and adds to an internal result as
+     * 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) {
+      if (index >= mappedRanges.size()) {
+        return null;
+      }
+      List<MappedRange> seenMappedRanges = new ArrayList<>();
+      MappedRange lastSeen = mappedRanges.get(index);
+      seenMappedRanges.add(lastSeen);
+      if (lastSeen.minifiedRange == null) {
+        return new MappedRangeResult(
+            NO_RANGE_FROM, NO_RANGE_FROM, index + 1, lastSeen, seenMappedRanges);
+      }
+      int startMinifiedPosition = lastSeen.minifiedRange.from;
+      int startOriginalPosition =
+          lastSeen.originalRange == null ? NO_RANGE_FROM : lastSeen.originalRange.from;
+      for (int i = index + 1; i < mappedRanges.size(); i++) {
+        MappedRange thisMappedRange = mappedRanges.get(i);
+        // We assume that if we see a mapping where the minified range is 0 then we will not find
+        // another mapping where it is not null.
+        if (thisMappedRange.minifiedRange == null) {
+          assert !lastSeen.signature.equals(thisMappedRange.signature);
+          break;
+        }
+        // Otherwise break if we see a signature that is not equal to the current one and it
+        // has another minified range meaning it is not an inlinee.
+        if (!thisMappedRange.signature.equals(lastSeen.signature)
+            && !thisMappedRange.minifiedRange.equals(lastSeen.minifiedRange)) {
+          break;
+        }
+        seenMappedRanges.add(thisMappedRange);
+        lastSeen = thisMappedRange;
+      }
+      return new MappedRangeResult(
+          startMinifiedPosition,
+          startOriginalPosition,
+          index + seenMappedRanges.size(),
+          lastSeen,
+          seenMappedRanges);
+    }
+
+    private List<MappedRange> composeMappedRangesForMethod(
+        List<MappedRange> existingRanges, List<MappedRange> newRanges)
+        throws MappingComposeException {
+      assert !newRanges.isEmpty();
+      if (existingRanges == null || existingRanges.isEmpty()) {
+        return newRanges;
+      }
+      // The new minified ranges may have ranges that range over positions that has additional
+      // information, such as inlinees:
+      // Existing:
+      //  1:2:void caller():14:15 -> x
+      //  3:3:void inlinee():42:42 -> x
+      //  3:3:void caller():16:16 -> x
+      //  4:10:void caller():17:23 -> x
+      //  ...
+      // New mapping:
+      //  1:5:void x():1:5 -> y
+      //  6:10:void x():6:6 -> y
+      //  ...
+      // It is important that we therefore split up the new ranges to map everything back to the
+      // existing original mappings:
+      // Resulting mapping:
+      //  1:2:void caller():14:15 -> y
+      //  3:3:void inlinee():42:42 -> y
+      //  3:3:void caller():16:16 -> y
+      //  4:5:void caller():17:18 -> y
+      //  6:10:void caller():19:19 -> y
+      //  ...
+      Int2ReferenceMap<List<MappedRange>> mappedRangesForPosition =
+          getExistingMapping(existingRanges);
+      List<MappedRange> newComposedRanges = new ArrayList<>();
+      for (int i = 0; i < newRanges.size(); i++) {
+        if (isInlineMappedRange(newRanges, i)) {
+          throw new MappingComposeException(
+              "Unexpected inline frame '" + existingRanges.get(i) + "' in composing map.");
+        }
+        MappedRange newRange = newRanges.get(i);
+        if (newRange.originalRange == null) {
+          List<MappedRange> existingMappedRanges = mappedRangesForPosition.get(NO_RANGE_FROM);
+          assert existingMappedRanges.size() <= 1;
+          if (existingMappedRanges.isEmpty()) {
+            newComposedRanges.add(newRange);
+          } else {
+            MappedRange existingRange = existingMappedRanges.get(0);
+            newComposedRanges.add(
+                new MappedRange(
+                    newRange.minifiedRange,
+                    existingRange.signature,
+                    existingRange.originalRange,
+                    newRange.renamedName));
+          }
+        } else {
+          // First check if the original range matches the existing minified range.
+          List<MappedRange> existingMappedRanges =
+              mappedRangesForPosition.get(newRange.originalRange.from);
+          if (existingMappedRanges == null) {
+            throw new MappingComposeException(
+                "Unexpected missing original position for '" + newRange + "'.");
+          }
+          if (ListUtils.last(existingMappedRanges).minifiedRange.equals(newRange.originalRange)) {
+            computeComposedMappedRange(
+                newComposedRanges,
+                newRange,
+                existingMappedRanges,
+                newRange.minifiedRange.from,
+                newRange.minifiedRange.to);
+          } else {
+            // Otherwise, we have a situation where the minified range references over multiple
+            // existing ranges. We simply chop op when the range changes on the right hand side. To
+            // ensure we do not mess up the spans from the original range, we have to check if the
+            // current starting position is inside an original range, and then chop it off.
+            // Similarly, when writing the last block, we have to cut it off to match.
+            int lastStartingMinifiedFrom = newRange.minifiedRange.from;
+            for (int position = newRange.minifiedRange.from;
+                position <= newRange.minifiedRange.to;
+                position++) {
+              List<MappedRange> existingMappedRangesForPosition =
+                  mappedRangesForPosition.get(newRange.getOriginalLineNumber(position));
+              if (existingMappedRangesForPosition == null) {
+                throw new MappingComposeException(
+                    "Unexpected missing original position for '" + newRange + "'.");
+              }
+              if (!ListUtils.last(existingMappedRanges)
+                  .minifiedRange
+                  .equals(ListUtils.last(existingMappedRangesForPosition).minifiedRange)) {
+                computeComposedMappedRange(
+                    newComposedRanges,
+                    newRange,
+                    existingMappedRanges,
+                    lastStartingMinifiedFrom,
+                    position - 1);
+                lastStartingMinifiedFrom = position;
+                existingMappedRanges = existingMappedRangesForPosition;
+              }
+            }
+            computeComposedMappedRange(
+                newComposedRanges,
+                newRange,
+                existingMappedRanges,
+                lastStartingMinifiedFrom,
+                newRange.minifiedRange.to);
+          }
+        }
+      }
+      return newComposedRanges;
+    }
+
+    /***
+     * Builds a position to mapped ranges for mappings for looking up all mapped ranges for a given
+     * position.
+     */
+    private Int2ReferenceMap<List<MappedRange>> getExistingMapping(
+        List<MappedRange> existingRanges) {
+      Int2ReferenceMap<List<MappedRange>> mappedRangesForPosition =
+          new Int2ReferenceOpenHashMap<>();
+      List<MappedRange> currentRangesForPosition = new ArrayList<>();
+      for (int i = 0; i < existingRanges.size(); i++) {
+        MappedRange mappedRange = existingRanges.get(i);
+        currentRangesForPosition.add(mappedRange);
+        if (!isInlineMappedRange(existingRanges, i)) {
+          if (mappedRange.minifiedRange == null) {
+            mappedRangesForPosition.put(NO_RANGE_FROM, currentRangesForPosition);
+          } else {
+            for (int position = mappedRange.minifiedRange.from;
+                position <= mappedRange.minifiedRange.to;
+                position++) {
+              mappedRangesForPosition.put(position, currentRangesForPosition);
+            }
+          }
+          currentRangesForPosition = new ArrayList<>();
+        }
+      }
+      return mappedRangesForPosition;
+    }
+
+    private void computeComposedMappedRange(
+        List<MappedRange> newComposedRanges,
+        MappedRange newMappedRange,
+        List<MappedRange> existingMappedRanges,
+        int lastStartingMinifiedFrom,
+        int position) {
+      Range existingRange = existingMappedRanges.get(0).minifiedRange;
+      assert existingMappedRanges.stream().allMatch(x -> x.minifiedRange.equals(existingRange));
+      Range newMinifiedRange = new Range(lastStartingMinifiedFrom, position);
+      boolean copyOriginalRange = existingRange.equals(newMappedRange.originalRange);
+      for (MappedRange existingMappedRange : existingMappedRanges) {
+        Range existingOriginalRange = existingMappedRange.originalRange;
+        Range newOriginalRange;
+        if (copyOriginalRange || existingOriginalRange.span() == 1) {
+          newOriginalRange = existingOriginalRange;
+        } else {
+          // Find the window that the new range points to into the original range.
+          int existingMinifiedPos = newMappedRange.getOriginalLineNumber(lastStartingMinifiedFrom);
+          int newOriginalStart = existingMappedRange.getOriginalLineNumber(existingMinifiedPos);
+          if (newMappedRange.originalRange.span() == 1) {
+            newOriginalRange = new Range(newOriginalStart, newOriginalStart);
+          } else {
+            assert newMinifiedRange.span() <= existingOriginalRange.span();
+            newOriginalRange =
+                new Range(newOriginalStart, newOriginalStart + newMinifiedRange.span() - 1);
+          }
+        }
+        newComposedRanges.add(
+            new MappedRange(
+                newMinifiedRange,
+                existingMappedRange.signature,
+                newOriginalRange,
+                newMappedRange.renamedName));
+      }
+    }
+
+    private boolean isInlineMappedRange(List<MappedRange> mappedRanges, int index) {
+      if (index + 1 >= mappedRanges.size()) {
+        return false;
+      }
+      return mappedRanges
+          .get(index)
+          .minifiedRange
+          .equals(mappedRanges.get(index + 1).minifiedRange);
+    }
+
     public void write(ChainableStringConsumer consumer) {
       consumer.accept(originalName).accept(" -> ").accept(renamedName).accept(":\n");
       // TODO(b/241763080): Support mapping information.
-      writeFieldNames(consumer);
-      // TODO(b/241763080): Support function mappings.
+      writeFields(consumer);
+      writeMethods(consumer);
     }
 
-    private void writeFieldNames(ChainableStringConsumer consumer) {
+    private void writeFields(ChainableStringConsumer consumer) {
       ArrayList<MemberNaming> fieldNamings = new ArrayList<>();
       for (List<MemberNaming> namingsForKey : fieldMembers.values()) {
         fieldNamings.addAll(namingsForKey);
@@ -125,5 +406,53 @@
       fieldNamings.forEach(
           naming -> consumer.accept(INDENTATION).accept(naming.toString()).accept("\n"));
     }
+
+    private void writeMethods(ChainableStringConsumer consumer) {
+      Map<String, List<MappedRange>> signatureToMappedRanges = new HashMap<>();
+      methodMembers
+          .values()
+          .forEach(
+              segmentTree -> {
+                segmentTree.visitSegments(
+                    mappedRanges -> {
+                      MethodSignature originalSignature = ListUtils.last(mappedRanges).signature;
+                      List<MappedRange> put =
+                          signatureToMappedRanges.put(
+                              originalSignature.getName() + "_" + originalSignature, mappedRanges);
+                      assert put == null;
+                    });
+              });
+      ArrayList<String> strings = new ArrayList<>(signatureToMappedRanges.keySet());
+      Collections.sort(strings);
+      for (String key : strings) {
+        signatureToMappedRanges
+            .get(key)
+            .forEach(
+                mappedRange ->
+                    consumer.accept(INDENTATION).accept(mappedRange.toString()).accept("\n"));
+      }
+    }
+
+    private static class MappedRangeResult {
+
+      private final int startMinifiedPosition;
+      private final int startOriginalPosition;
+      private final int endIndex;
+      private final MappedRange lastRange;
+      private final List<MappedRange> allRanges;
+
+      public MappedRangeResult(
+          int startMinifiedPosition,
+          int startOriginalPosition,
+          int endIndex,
+          MappedRange lastRange,
+          List<MappedRange> allRanges) {
+        this.startMinifiedPosition = startMinifiedPosition;
+        this.startOriginalPosition = startOriginalPosition;
+        this.endIndex = endIndex;
+        this.lastRange = lastRange;
+        this.allRanges = allRanges;
+      }
+    }
   }
 }
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 bf0a8dd..0c833bd 100644
--- a/src/main/java/com/android/tools/r8/naming/MemberNaming.java
+++ b/src/main/java/com/android/tools/r8/naming/MemberNaming.java
@@ -162,6 +162,10 @@
       }
     }
 
+    public String getName() {
+      return name;
+    }
+
     enum SignatureKind {
       METHOD,
       FIELD
diff --git a/src/main/java/com/android/tools/r8/utils/SegmentTree.java b/src/main/java/com/android/tools/r8/utils/SegmentTree.java
index 1fc95b0..9962b1c 100644
--- a/src/main/java/com/android/tools/r8/utils/SegmentTree.java
+++ b/src/main/java/com/android/tools/r8/utils/SegmentTree.java
@@ -6,9 +6,10 @@
 
 import java.util.Map;
 import java.util.TreeMap;
+import java.util.function.Consumer;
 
 /**
- * Implementation of a discrete segment tree where intervals are specified by their start end and
+ * Implementation of a discrete segment tree where intervals are specified by their start and end
  * point. Both points are considered part of the interval.
  */
 public class SegmentTree<V> {
@@ -79,4 +80,21 @@
   public int size() {
     return size;
   }
+
+  public void removeSegment(int start) {
+    if (internalTree.remove(start) != null) {
+      size = size - 1;
+    }
+  }
+
+  public void visitSegments(Consumer<V> consumer) {
+    internalTree
+        .values()
+        .forEach(
+            segment -> {
+              if (segment != null) {
+                consumer.accept(segment);
+              }
+            });
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeChangeLineNumberTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeChangeLineNumberTest.java
new file mode 100644
index 0000000..bffa4b3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeChangeLineNumberTest.java
@@ -0,0 +1,45 @@
+// Copyright (c) 2022, 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 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 ComposeChangeLineNumberTest extends TestBase {
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  private static final String mappingFoo =
+      StringUtils.lines("com.foo -> a:", "    1:1:void m():42:42 -> m");
+  private static final String mappingBar =
+      StringUtils.lines("a -> b:", "    2:2:void m():1:1 -> m");
+  private static final String mappingResult =
+      StringUtils.lines("com.foo -> b:", "    2:2:void m():42:42 -> m");
+
+  @Test
+  public void testCompose() throws Exception {
+    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromString(mappingFoo);
+    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromString(mappingBar);
+    String composed = MappingComposer.compose(mappingForFoo, mappingForBar);
+    assertEquals(mappingResult, composed);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeChangeMethodAndLineNumberTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeChangeMethodAndLineNumberTest.java
new file mode 100644
index 0000000..3e4270c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeChangeMethodAndLineNumberTest.java
@@ -0,0 +1,45 @@
+// Copyright (c) 2022, 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 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 ComposeChangeMethodAndLineNumberTest extends TestBase {
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  private static final String mappingFoo =
+      StringUtils.lines("com.foo -> a:", "    1:1:void m1():42:42 -> m2");
+  private static final String mappingBar =
+      StringUtils.lines("a -> b:", "    2:2:void m2():1:1 -> m3");
+  private static final String mappingResult =
+      StringUtils.lines("com.foo -> b:", "    2:2:void m1():42:42 -> m3");
+
+  @Test
+  public void testCompose() throws Exception {
+    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromString(mappingFoo);
+    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromString(mappingBar);
+    String composed = MappingComposer.compose(mappingForFoo, mappingForBar);
+    assertEquals(mappingResult, composed);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeChangeMethodTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeChangeMethodTest.java
new file mode 100644
index 0000000..03b2baa
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeChangeMethodTest.java
@@ -0,0 +1,44 @@
+// Copyright (c) 2022, 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 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 ComposeChangeMethodTest extends TestBase {
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  private static final String mappingFoo =
+      StringUtils.lines("com.foo -> a:", "    void f1() -> f2");
+  private static final String mappingBar = StringUtils.lines("a -> b:", "    void f2() -> f3");
+  private static final String mappingResult =
+      StringUtils.lines("com.foo -> b:", "    void f1() -> f3");
+
+  @Test
+  public void testCompose() throws Exception {
+    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromString(mappingFoo);
+    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromString(mappingBar);
+    String composed = MappingComposer.compose(mappingForFoo, mappingForBar);
+    assertEquals(mappingResult, composed);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeDifferentMethodTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeDifferentMethodTest.java
new file mode 100644
index 0000000..b0ceccc
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeDifferentMethodTest.java
@@ -0,0 +1,48 @@
+// Copyright (c) 2022, 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 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 ComposeDifferentMethodTest extends TestBase {
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  // Here we are testing that a shrinker could have changed the method signature. Since we have no
+  // right hand side mapping we have no other choice than to map the naming through. This would not
+  // be ok if we had overloads but any shrinker should put in line numbering for those.
+  private static final String mappingFoo =
+      StringUtils.lines("com.foo -> a:", "    void f1(int) -> f2");
+  private static final String mappingBar =
+      StringUtils.lines("a -> b:", "    int f2(boolean) -> f3");
+  private static final String mappingResult =
+      StringUtils.lines("com.foo -> b:", "    void f1(int) -> f3");
+
+  @Test
+  public void testCompose() throws Exception {
+    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromString(mappingFoo);
+    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromString(mappingBar);
+    String composed = MappingComposer.compose(mappingForFoo, mappingForBar);
+    assertEquals(mappingResult, composed);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeDifferentMethodWithLineNumberTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeDifferentMethodWithLineNumberTest.java
new file mode 100644
index 0000000..a58342f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeDifferentMethodWithLineNumberTest.java
@@ -0,0 +1,47 @@
+// Copyright (c) 2022, 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 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 ComposeDifferentMethodWithLineNumberTest extends TestBase {
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  private static final String mappingFoo =
+      StringUtils.lines(
+          "com.foo -> a:", "    1:1:int f1(boolean) -> f2", "    2:2:void f1(int) -> f2");
+  private static final String mappingBar =
+      StringUtils.lines("a -> b:", "    8:8:void f2(boolean):2:2 -> f3");
+  private static final String mappingResult =
+      StringUtils.lines(
+          "com.foo -> b:", "    1:1:int f1(boolean) -> f2", "    8:8:void f1(int) -> f3");
+
+  @Test
+  public void testCompose() throws Exception {
+    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromString(mappingFoo);
+    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromString(mappingBar);
+    String composed = MappingComposer.compose(mappingForFoo, mappingForBar);
+    assertEquals(mappingResult, composed);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeInlineFollowedByLineNumberChangeTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeInlineFollowedByLineNumberChangeTest.java
new file mode 100644
index 0000000..8c6116e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeInlineFollowedByLineNumberChangeTest.java
@@ -0,0 +1,53 @@
+// Copyright (c) 2022, 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 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 ComposeInlineFollowedByLineNumberChangeTest extends TestBase {
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  private static final String mappingFoo =
+      StringUtils.lines(
+          "com.foo -> a:",
+          "    1:1:void inlinee2():42:42 -> x",
+          "    1:1:void foo.bar.baz.inlinee1():41 -> x",
+          "    1:1:void caller():40 -> x");
+  private static final String mappingBar =
+      StringUtils.lines("a -> b:", "    2:2:void x():1:1 -> y");
+  private static final String mappingResult =
+      StringUtils.lines(
+          "com.foo -> b:",
+          "    2:2:void inlinee2():42:42 -> y",
+          "    2:2:void foo.bar.baz.inlinee1():41 -> y",
+          "    2:2:void caller():40 -> y");
+
+  @Test
+  public void testCompose() throws Exception {
+    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromString(mappingFoo);
+    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromString(mappingBar);
+    String composed = MappingComposer.compose(mappingForFoo, mappingForBar);
+    assertEquals(mappingResult, composed);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeSplitRangeStartTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeSplitRangeStartTest.java
new file mode 100644
index 0000000..84bceed
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeSplitRangeStartTest.java
@@ -0,0 +1,57 @@
+// Copyright (c) 2022, 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 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 ComposeSplitRangeStartTest extends TestBase {
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  private static final String mappingFoo =
+      StringUtils.lines(
+          "com.foo -> a:", "    1:5:void m():41:45 -> x", "    6:10:void m():56:60 -> x");
+  private static final String mappingBar =
+      StringUtils.lines(
+          "a -> b:",
+          "    11:12:void x():1:2 -> y",
+          "    13:17:void x():3:7 -> y",
+          "    18:24:void x():8:8 -> y",
+          "    25:26:void x():9:10 -> y");
+  private static final String mappingResult =
+      StringUtils.lines(
+          "com.foo -> b:",
+          "    11:12:void m():41:42 -> y",
+          "    13:15:void m():43:45 -> y",
+          "    16:17:void m():56:57 -> y",
+          "    18:24:void m():58:58 -> y",
+          "    25:26:void m():59:60 -> y");
+
+  @Test
+  public void testCompose() throws Exception {
+    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromString(mappingFoo);
+    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromString(mappingBar);
+    String composed = MappingComposer.compose(mappingForFoo, mappingForBar);
+    assertEquals(mappingResult, composed);
+  }
+}