Enable combining class name mappers to build partial results

Bug: b/234758957
Bug: b/201666766
Change-Id: I455d73a2c1a0b56ddbe83942dfe15f1de80a0537
diff --git a/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java b/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java
index 9f01beb..a3dbc6c 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java
@@ -65,7 +65,6 @@
 
     private ImmutableMap<String, ClassNamingForNameMapper> buildClassNameMappings() {
       ImmutableMap.Builder<String, ClassNamingForNameMapper> builder = ImmutableMap.builder();
-      builder.orderEntriesByValue(Comparator.comparing(x -> x.originalName));
       mapping.forEach(
           (renamedName, valueBuilder) -> builder.put(renamedName, valueBuilder.build()));
       return builder.build();
@@ -242,6 +241,34 @@
     return originalSourceFiles.get(typeName);
   }
 
+  public ClassNameMapper combine(ClassNameMapper other) {
+    ImmutableMap.Builder<String, ClassNamingForNameMapper> builder = ImmutableMap.builder();
+    Map<String, ClassNamingForNameMapper> otherClassMappings = other.getClassNameMappings();
+    for (Entry<String, ClassNamingForNameMapper> mappingEntry : classNameMappings.entrySet()) {
+      ClassNamingForNameMapper otherMapping = otherClassMappings.get(mappingEntry.getKey());
+      if (otherMapping == null) {
+        builder.put(mappingEntry);
+      } else {
+        builder.put(mappingEntry.getKey(), mappingEntry.getValue().combine(otherMapping));
+      }
+    }
+    otherClassMappings.forEach(
+        (otherMappingClass, otherMapping) -> {
+          // Collisions are handled above so only take non-existing mappings.
+          if (!classNameMappings.containsKey(otherMappingClass)) {
+            builder.put(otherMappingClass, otherMapping);
+          }
+        });
+    otherClassMappings.forEach(builder::put);
+    Set<MapVersionMappingInformation> newMapVersions = new LinkedHashSet<>(getMapVersions());
+    newMapVersions.addAll(other.getMapVersions());
+    Map<String, String> newSourcesFiles = new HashMap<>(originalSourceFiles);
+    // This will overwrite existing source files but the chance of that happening should be very
+    // slim.
+    newSourcesFiles.putAll(other.originalSourceFiles);
+    return new ClassNameMapper(builder.build(), newMapVersions, newSourcesFiles);
+  }
+
   @Override
   public boolean hasMapping(DexType type) {
     String decoded = descriptorToJavaType(type.descriptor.toString());
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 c1e5de7..352c555 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNamingForNameMapper.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNamingForNameMapper.java
@@ -265,6 +265,40 @@
     return mappedRangesByRenamedName.get(renamedName);
   }
 
+  public boolean isEmpty() {
+    return methodMembers.isEmpty() && fieldMembers.isEmpty();
+  }
+
+  public ClassNamingForNameMapper combine(ClassNamingForNameMapper otherMapping) {
+    if (!originalName.equals(otherMapping.originalName)) {
+      throw new RuntimeException(
+          "Cannot combine mapping for "
+              + renamedName
+              + " because it maps back to both "
+              + originalName
+              + " and "
+              + otherMapping.originalName
+              + ".");
+    }
+    if (!renamedName.equals(otherMapping.renamedName)) {
+      throw new RuntimeException(
+          "Cannot combine mapping for "
+              + originalName
+              + " because it maps forward to both "
+              + originalName
+              + " and "
+              + otherMapping.originalName
+              + ".");
+    }
+    if (this.isEmpty()) {
+      return otherMapping;
+    } else if (otherMapping.isEmpty()) {
+      return this;
+    } else {
+      throw new RuntimeException("R8 Retrace do not support merging of partial class mappings.");
+    }
+  }
+
   @Override
   public MemberNaming lookup(Signature renamedSignature) {
     if (renamedSignature.kind() == SignatureKind.METHOD) {
diff --git a/src/test/java/com/android/tools/r8/naming/ProguardMapReaderTest.java b/src/test/java/com/android/tools/r8/naming/ProguardMapReaderTest.java
index 4e56011..d94ad7b 100644
--- a/src/test/java/com/android/tools/r8/naming/ProguardMapReaderTest.java
+++ b/src/test/java/com/android/tools/r8/naming/ProguardMapReaderTest.java
@@ -56,17 +56,20 @@
 
   @Test
   public void roundTripTest() throws IOException {
-    ClassNameMapper firstMapper = ClassNameMapper.mapperFromFile(Paths.get(ROOT, EXAMPLE_MAP));
-    ClassNameMapper secondMapper = ClassNameMapper.mapperFromString(firstMapper.toString());
+    ClassNameMapper firstMapper =
+        ClassNameMapper.mapperFromFile(Paths.get(ROOT, EXAMPLE_MAP)).sorted();
+    ClassNameMapper secondMapper =
+        ClassNameMapper.mapperFromString(firstMapper.toString()).sorted();
     Assert.assertEquals(firstMapper, secondMapper);
   }
 
   @Test
   public void roundTripTestWithLeadingBOM() throws IOException {
-    ClassNameMapper firstMapper = ClassNameMapper.mapperFromFile(Paths.get(ROOT, EXAMPLE_MAP));
+    ClassNameMapper firstMapper =
+        ClassNameMapper.mapperFromFile(Paths.get(ROOT, EXAMPLE_MAP)).sorted();
     assertTrue(firstMapper.toString().charAt(0) != StringUtils.BOM);
     ClassNameMapper secondMapper =
-        ClassNameMapper.mapperFromString(StringUtils.BOM + firstMapper.toString());
+        ClassNameMapper.mapperFromString(StringUtils.BOM + firstMapper.toString()).sorted();
     assertTrue(secondMapper.toString().charAt(0) != StringUtils.BOM);
     Assert.assertEquals(firstMapper, secondMapper);
     byte[] bytes = Files.readAllBytes(Paths.get(ROOT, EXAMPLE_MAP));
@@ -76,7 +79,7 @@
     assertEquals(0xef, Byte.toUnsignedLong(bytes[0]));
     assertEquals(0xbb, Byte.toUnsignedLong(bytes[1]));
     assertEquals(0xbf, Byte.toUnsignedLong(bytes[2]));
-    ClassNameMapper thirdMapper = ClassNameMapper.mapperFromFile(mapFileWithBOM);
+    ClassNameMapper thirdMapper = ClassNameMapper.mapperFromFile(mapFileWithBOM).sorted();
     assertTrue(thirdMapper.toString().charAt(0) != StringUtils.BOM);
     Assert.assertEquals(firstMapper, thirdMapper);
   }
@@ -93,7 +96,8 @@
             "" + StringUtils.BOM,
             StringUtils.BOM + " " + StringUtils.BOM);
     for (String whitespace : ws) {
-      ClassNameMapper firstMapper = ClassNameMapper.mapperFromFile(Paths.get(ROOT, EXAMPLE_MAP));
+      ClassNameMapper firstMapper =
+          ClassNameMapper.mapperFromFile(Paths.get(ROOT, EXAMPLE_MAP)).sorted();
       assertTrue(firstMapper.toString().charAt(0) != StringUtils.BOM);
       StringBuilder buildWithWhitespace = new StringBuilder();
       char prevChar = '\0';
@@ -114,13 +118,13 @@
         prevChar = c;
       }
       ClassNameMapper secondMapper =
-          ClassNameMapper.mapperFromString(buildWithWhitespace.toString());
+          ClassNameMapper.mapperFromString(buildWithWhitespace.toString()).sorted();
       assertFalse(firstMapper.toString().contains("" + StringUtils.BOM));
       Assert.assertEquals(firstMapper, secondMapper);
       byte[] bytes = Files.readAllBytes(Paths.get(ROOT, EXAMPLE_MAP));
       assertNotEquals(0xef, Byte.toUnsignedLong(bytes[0]));
       Path mapFileWithBOM = writeTextToTempFile(StringUtils.BOM + firstMapper.toString());
-      ClassNameMapper thirdMapper = ClassNameMapper.mapperFromFile(mapFileWithBOM);
+      ClassNameMapper thirdMapper = ClassNameMapper.mapperFromFile(mapFileWithBOM).sorted();
       assertTrue(thirdMapper.toString().charAt(0) != StringUtils.BOM);
       Assert.assertEquals(firstMapper, thirdMapper);
     }