[MappingCompose] Move composition into its own builder and add fields

Bug: b/241763080
Change-Id: I0bfeb4aa1088a1a74c2ad8fda02e6673f2eb3c2b
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 2cf9407..f9266b0 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNamingForNameMapper.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNamingForNameMapper.java
@@ -3,8 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.naming;
 
-import static com.android.tools.r8.utils.FunctionUtils.ignoreArgument;
-
 import com.android.tools.r8.naming.MemberNaming.FieldSignature;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.naming.MemberNaming.Signature;
@@ -108,12 +106,6 @@
       return range;
     }
 
-    public void addMappedRange(MappedRange mappedRange) {
-      mappedRangesByName
-          .computeIfAbsent(mappedRange.renamedName, ignoreArgument(ArrayList::new))
-          .add(mappedRange);
-    }
-
     @Override
     public void addMappingInformation(
         MappingInformation info, Consumer<MappingInformation> onProhibitedAddition) {
@@ -473,15 +465,6 @@
     return result;
   }
 
-  public void compose(Builder classNameMapperBuilder) {
-    // TODO(b/241763080): Account for previous mappings.
-    classNameMapperBuilder.additionalMappingInfo.addAll(additionalMappingInfo);
-    forAllFieldNaming(classNameMapperBuilder::addMemberEntry);
-    for (MappedRangesOfName ranges : mappedRangesByRenamedName.values()) {
-      ranges.mappedRanges.forEach(classNameMapperBuilder::addMappedRange);
-    }
-  }
-
   /**
    * MappedRange describes an (original line numbers, signature) <-> (minified line numbers)
    * mapping. It can describe 3 different things:
diff --git a/src/main/java/com/android/tools/r8/naming/ComposingBuilder.java b/src/main/java/com/android/tools/r8/naming/ComposingBuilder.java
new file mode 100644
index 0000000..badc6fe
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/naming/ComposingBuilder.java
@@ -0,0 +1,129 @@
+// 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.naming;
+
+import static com.android.tools.r8.utils.FunctionUtils.ignoreArgument;
+
+import com.android.tools.r8.utils.ChainableStringConsumer;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ComposingBuilder {
+
+  private final Map<String, ComposingClassBuilder> mapping = new HashMap<>();
+
+  public void compose(ClassNamingForNameMapper classMapping) throws MappingComposeException {
+    String originalName = classMapping.originalName;
+    ComposingClassBuilder composingClassBuilder = mapping.get(originalName);
+    String renamedName = classMapping.renamedName;
+    if (composingClassBuilder == null) {
+      composingClassBuilder = new ComposingClassBuilder(originalName, renamedName);
+    } else {
+      composingClassBuilder.setRenamedName(renamedName);
+      mapping.remove(originalName);
+    }
+    ComposingClassBuilder previousMapping = mapping.put(renamedName, composingClassBuilder);
+    if (previousMapping != null) {
+      throw new MappingComposeException(
+          "Duplicate class mapping. Both '"
+              + previousMapping.getOriginalName()
+              + "' and '"
+              + originalName
+              + "' maps to '"
+              + renamedName
+              + "'.");
+    }
+    composingClassBuilder.compose(classMapping);
+  }
+
+  @Override
+  public String toString() {
+    List<ComposingClassBuilder> classBuilders = new ArrayList<>(mapping.values());
+    classBuilders.sort(Comparator.comparing(ComposingClassBuilder::getOriginalName));
+    StringBuilder sb = new StringBuilder();
+    ChainableStringConsumer wrap = ChainableStringConsumer.wrap(sb::append);
+    for (ComposingClassBuilder classBuilder : classBuilders) {
+      classBuilder.write(wrap);
+    }
+    return sb.toString();
+  }
+
+  public static class ComposingClassBuilder {
+
+    private static final String INDENTATION = "    ";
+
+    private final String originalName;
+    private String renamedName;
+    private final Map<String, List<MemberNaming>> fieldMembers = new HashMap<>();
+
+    private ComposingClassBuilder(String originalName, String renamedName) {
+      this.originalName = originalName;
+      this.renamedName = renamedName;
+    }
+
+    public void setRenamedName(String renamedName) {
+      this.renamedName = renamedName;
+    }
+
+    public String getOriginalName() {
+      return originalName;
+    }
+
+    public String getRenamedName() {
+      return renamedName;
+    }
+
+    public void compose(ClassNamingForNameMapper mapper) throws MappingComposeException {
+      mapper.forAllFieldNaming(
+          fieldNaming -> {
+            List<MemberNaming> memberNamings = fieldMembers.get(fieldNaming.getOriginalName());
+            if (memberNamings == null) {
+              fieldMembers
+                  .computeIfAbsent(fieldNaming.getRenamedName(), ignoreArgument(ArrayList::new))
+                  .add(fieldNaming);
+              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.
+            for (int i = 0; i < memberNamings.size(); i++) {
+              MemberNaming memberNaming = memberNamings.get(i);
+              assert memberNaming.getRenamedName().equals(fieldNaming.getOriginalName());
+              if (memberNaming.renamedSignature.equals(fieldNaming.getOriginalSignature())) {
+                memberNamings.set(
+                    i,
+                    new MemberNaming(
+                        memberNaming.getOriginalSignature(), fieldNaming.getRenamedName()));
+                return;
+              }
+            }
+            throw new MappingComposeException(
+                "Unable to compose field naming '"
+                    + fieldNaming
+                    + "' since the original type has changed.");
+          });
+    }
+
+    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.
+    }
+
+    private void writeFieldNames(ChainableStringConsumer consumer) {
+      ArrayList<MemberNaming> fieldNamings = new ArrayList<>();
+      for (List<MemberNaming> namingsForKey : fieldMembers.values()) {
+        fieldNamings.addAll(namingsForKey);
+      }
+      fieldNamings.sort(Comparator.comparing(MemberNaming::getOriginalName));
+      fieldNamings.forEach(
+          naming -> consumer.accept(INDENTATION).accept(naming.toString()).accept("\n"));
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/naming/MappingComposeException.java b/src/main/java/com/android/tools/r8/naming/MappingComposeException.java
new file mode 100644
index 0000000..f443d0e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/naming/MappingComposeException.java
@@ -0,0 +1,15 @@
+// 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.naming;
+
+import com.android.tools.r8.Keep;
+
+@Keep
+public class MappingComposeException extends Exception {
+
+  public MappingComposeException(String message) {
+    super(message);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/naming/MappingComposer.java b/src/main/java/com/android/tools/r8/naming/MappingComposer.java
index 242aff0..55ef9ee 100644
--- a/src/main/java/com/android/tools/r8/naming/MappingComposer.java
+++ b/src/main/java/com/android/tools/r8/naming/MappingComposer.java
@@ -4,35 +4,24 @@
 
 package com.android.tools.r8.naming;
 
-import com.android.tools.r8.position.Position;
-
 /**
  * MappingComposer is a utility to do composition of mapping files to map line numbers correctly
  * when having shrunken input that will end up using DEX PC mappings.
  */
 public class MappingComposer {
 
-  public static ClassNameMapper compose(ClassNameMapper... classNameMappers) {
+  public static String compose(ClassNameMapper... classNameMappers) throws MappingComposeException {
     assert classNameMappers.length > 0;
     if (classNameMappers.length == 1) {
-      return classNameMappers[0];
+      return classNameMappers[0].toString();
     }
-    ClassNameMapper.Builder builder = ClassNameMapper.builder();
+    ComposingBuilder builder = new ComposingBuilder();
     for (ClassNameMapper classNameMapper : classNameMappers) {
-      compose(builder, classNameMapper);
+      for (ClassNamingForNameMapper classMapping :
+          classNameMapper.getClassNameMappings().values()) {
+        builder.compose(classMapping);
+      }
     }
-    return builder.build();
-  }
-
-  private static void compose(ClassNameMapper.Builder builder, ClassNameMapper toAdd) {
-    toAdd
-        .getClassNameMappings()
-        .forEach(
-            (key, classNamingForNameMapper) -> {
-              ClassNamingForNameMapper.Builder classNameMapperBuilder =
-                  builder.classNamingBuilder(
-                      key, classNamingForNameMapper.originalName, Position.UNKNOWN);
-              classNamingForNameMapper.compose(classNameMapperBuilder);
-            });
+    return builder.toString();
   }
 }
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeDistinctClassesTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeDistinctClassesTest.java
index 3bdbedc..706f7ef 100644
--- a/src/test/java/com/android/tools/r8/mappingcompose/ComposeDistinctClassesTest.java
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeDistinctClassesTest.java
@@ -28,16 +28,14 @@
     return getTestParameters().withNoneRuntime().build();
   }
 
-  private static final String mappingFoo =
-      StringUtils.lines("com.foo -> a:", "    1:2:void m1() -> b");
-  private static final String mappingBar =
-      StringUtils.lines("com.bar -> b:", "    3:4:void m2() -> b");
+  private static final String mappingFoo = StringUtils.lines("com.foo -> a:");
+  private static final String mappingBar = StringUtils.lines("com.bar -> b:");
 
   @Test
   public void testCompose() throws Exception {
     ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromString(mappingFoo);
     ClassNameMapper mappingForBar = ClassNameMapper.mapperFromString(mappingBar);
-    ClassNameMapper composed = MappingComposer.compose(mappingForFoo, mappingForBar);
-    assertEquals(mappingBar + mappingFoo, composed.sorted().toString());
+    String composed = MappingComposer.compose(mappingForFoo, mappingForBar);
+    assertEquals(mappingBar + mappingFoo, composed);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeDuplicateClassMappingTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeDuplicateClassMappingTest.java
new file mode 100644
index 0000000..154d09d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeDuplicateClassMappingTest.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 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;
+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 ComposeDuplicateClassMappingTest 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 -> b:");
+  private static final String mappingBar = StringUtils.lines("a -> b:");
+
+  @Test
+  public void testCompose() throws Exception {
+    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromString(mappingFoo);
+    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromString(mappingBar);
+    MappingComposeException mappingComposeException =
+        assertThrows(
+            MappingComposeException.class,
+            () -> MappingComposer.compose(mappingForFoo, mappingForBar));
+    assertEquals(
+        "Duplicate class mapping. Both 'com.foo' and 'a' maps to 'b'.",
+        mappingComposeException.getMessage());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeFieldNameTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeFieldNameTest.java
new file mode 100644
index 0000000..e568a56
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeFieldNameTest.java
@@ -0,0 +1,43 @@
+// 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 ComposeFieldNameTest 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:", "    int f1 -> f2");
+  private static final String mappingBar = StringUtils.lines("a -> b:", "    int f2 -> f3");
+  private static final String mappingResult =
+      StringUtils.lines("com.foo -> b:", "    int 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/ComposeFieldWithAmbiguousTypeTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeFieldWithAmbiguousTypeTest.java
new file mode 100644
index 0000000..b6883aa
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeFieldWithAmbiguousTypeTest.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 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;
+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 ComposeFieldWithAmbiguousTypeTest 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:", "    int f1 -> f2");
+  private static final String mappingBar = StringUtils.lines("a -> b:", "    bool f2 -> f3");
+
+  @Test
+  public void testCompose() throws Exception {
+    ClassNameMapper mappingForFoo = ClassNameMapper.mapperFromString(mappingFoo);
+    ClassNameMapper mappingForBar = ClassNameMapper.mapperFromString(mappingBar);
+    MappingComposeException mappingComposeException =
+        assertThrows(
+            MappingComposeException.class,
+            () -> MappingComposer.compose(mappingForFoo, mappingForBar));
+    assertEquals(
+        "Unable to compose field naming 'bool f2 -> f3' since the original type has changed.",
+        mappingComposeException.getMessage());
+  }
+}