Stream the Proguard map to the consumer

Bug: 152939233
Change-Id: Ibba93a8b69294b2b0ffffd115209ac5935c9a286
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 4472714..e98d9b9 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.naming.MemberNaming.Signature;
 import com.android.tools.r8.position.Position;
 import com.android.tools.r8.utils.BiMapContainer;
+import com.android.tools.r8.utils.ChainableStringConsumer;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.ImmutableBiMap;
 import com.google.common.collect.ImmutableMap;
@@ -23,16 +24,14 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
-import java.io.StringWriter;
-import java.io.Writer;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.HashMap;
-import java.util.List;
+import java.util.Iterator;
 import java.util.Map;
+import java.util.Map.Entry;
 
 public class ClassNameMapper implements ProguardMap {
 
@@ -113,6 +112,10 @@
     this.classNameMappings = builder.build();
   }
 
+  private ClassNameMapper(ImmutableMap<String, ClassNamingForNameMapper> classNameMappings) {
+    this.classNameMappings = classNameMappings;
+  }
+
   public Map<String, ClassNamingForNameMapper> getClassNameMappings() {
     return classNameMappings;
   }
@@ -182,26 +185,41 @@
     return classNameMappings.isEmpty();
   }
 
-  public void write(Writer writer) throws IOException {
-    // Sort classes by their original name such that the generated Proguard map is deterministic
-    // (and easy to navigate manually).
-    List<ClassNamingForNameMapper> classNamingForNameMappers =
-        new ArrayList<>(classNameMappings.values());
-    classNamingForNameMappers.sort(Comparator.comparing(x -> x.originalName));
-    for (ClassNamingForNameMapper naming : classNamingForNameMappers) {
-      naming.write(writer);
+  public ClassNameMapper sorted() {
+    ImmutableMap.Builder<String, ClassNamingForNameMapper> builder = ImmutableMap.builder();
+    builder.orderEntriesByValue(Comparator.comparing(x -> x.originalName));
+    classNameMappings.forEach(builder::put);
+    return new ClassNameMapper(builder.build());
+  }
+
+  public boolean verifyIsSorted() {
+    Iterator<Entry<String, ClassNamingForNameMapper>> iterator =
+        getClassNameMappings().entrySet().iterator();
+    Iterator<Entry<String, ClassNamingForNameMapper>> sortedIterator =
+        sorted().getClassNameMappings().entrySet().iterator();
+    while (iterator.hasNext()) {
+      Entry<String, ClassNamingForNameMapper> entry = iterator.next();
+      Entry<String, ClassNamingForNameMapper> otherEntry = sortedIterator.next();
+      assert entry.getKey().equals(otherEntry.getKey());
+      assert entry.getValue() == otherEntry.getValue();
+    }
+    return true;
+  }
+
+  public void write(ChainableStringConsumer consumer) {
+    // Classes should be sorted by their original name such that the generated Proguard map is
+    // deterministic (and easy to navigate manually).
+    assert verifyIsSorted();
+    for (ClassNamingForNameMapper naming : getClassNameMappings().values()) {
+      naming.write(consumer);
     }
   }
 
   @Override
   public String toString() {
-    try {
-      StringWriter writer = new StringWriter();
-      write(writer);
-      return writer.toString();
-    } catch (IOException e) {
-      return e.toString();
-    }
+    StringBuilder builder = new StringBuilder();
+    write(ChainableStringConsumer.wrap(builder::append));
+    return builder.toString();
   }
 
   public BiMapContainer<String, String> getObfuscatedToOriginalMapping() {
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 ee02d05..5a00fcb 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNamingForNameMapper.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNamingForNameMapper.java
@@ -7,12 +7,10 @@
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.naming.MemberNaming.Signature;
 import com.android.tools.r8.naming.MemberNaming.Signature.SignatureKind;
+import com.android.tools.r8.utils.ChainableStringConsumer;
 import com.android.tools.r8.utils.ThrowingConsumer;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
-import java.io.IOException;
-import java.io.StringWriter;
-import java.io.Writer;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -297,19 +295,11 @@
     return methodMembers.values();
   }
 
-  void write(Writer writer) throws IOException {
-    writer.append(originalName);
-    writer.append(" -> ");
-    writer.append(renamedName);
-    writer.append(":\n");
+  void write(ChainableStringConsumer consumer) {
+    consumer.accept(originalName).accept(" -> ").accept(renamedName).accept(":\n");
 
-    // First print non-method MemberNamings.
-    forAllMemberNaming(
-        m -> {
-          if (!m.isMethodNaming()) {
-            writer.append("    ").append(m.toString()).append('\n');
-          }
-        });
+    // First print field member namings.
+    forAllFieldNaming(m -> consumer.accept("    ").accept(m.toString()).accept("\n"));
 
     // Sort MappedRanges by sequence number to restore construction order (original Proguard-map
     // input).
@@ -319,19 +309,15 @@
     }
     mappedRangesSorted.sort(Comparator.comparingInt(range -> range.sequenceNumber));
     for (MappedRange range : mappedRangesSorted) {
-      writer.append("    ").append(range.toString()).append('\n');
+      consumer.accept("    ").accept(range.toString()).accept("\n");
     }
   }
 
   @Override
   public String toString() {
-    try {
-      StringWriter writer = new StringWriter();
-      write(writer);
-      return writer.toString();
-    } catch (IOException e) {
-      return e.toString();
-    }
+    StringBuilder builder = new StringBuilder();
+    write(ChainableStringConsumer.wrap(builder::append));
+    return builder.toString();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapSupplier.java b/src/main/java/com/android/tools/r8/naming/ProguardMapSupplier.java
index d5b915b..ec947e8 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapSupplier.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapSupplier.java
@@ -3,10 +3,15 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.naming;
 
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.StringConsumer;
 import com.android.tools.r8.Version;
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.ChainableStringConsumer;
 import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.VersionProperties;
 import com.google.common.hash.Hasher;
 import com.google.common.hash.Hashing;
@@ -34,11 +39,20 @@
   }
 
   private final ClassNameMapper classNameMapper;
+  private final StringConsumer consumer;
   private final InternalOptions options;
+  private final Reporter reporter;
 
-  public ProguardMapSupplier(ClassNameMapper classNameMapper, InternalOptions options) {
-    this.classNameMapper = classNameMapper;
+  private ProguardMapSupplier(ClassNameMapper classNameMapper, InternalOptions options) {
+    assert classNameMapper != null;
+    assert !classNameMapper.isEmpty();
+    this.classNameMapper = classNameMapper.sorted();
+    this.consumer =
+        InternalOptions.assertionsEnabled()
+            ? new ProguardMapChecker(options.proguardMapConsumer)
+            : options.proguardMapConsumer;
     this.options = options;
+    this.reporter = options.reporter;
   }
 
   public static ProguardMapSupplier create(
@@ -47,32 +61,25 @@
   }
 
   public ProguardMapId writeProguardMap() {
-    String body = classNameMapper.toString();
-    assert body != null;
-    assert !body.trim().isEmpty();
-    ProguardMapId id = computeProguardMapId(body);
-    StringBuilder builder = new StringBuilder();
-    writeMarker(builder, id);
-    writeBody(builder, body);
-    String proguardMapContent = builder.toString();
-    assert validateProguardMapParses(proguardMapContent);
-    ExceptionUtils.withConsumeResourceHandler(
-        options.reporter, options.proguardMapConsumer, proguardMapContent);
-    ExceptionUtils.withFinishedResourceHandler(options.reporter, options.proguardMapConsumer);
+    ProguardMapId id = computeProguardMapId();
+    writeMarker(id);
+    writeBody();
+    ExceptionUtils.withFinishedResourceHandler(reporter, consumer);
     return id;
   }
 
-  private ProguardMapId computeProguardMapId(String body) {
-    Hasher hasher = Hashing.murmur3_32().newHasher();
-    body.codePoints().filter(c -> !Character.isWhitespace(c)).forEach(hasher::putInt);
-    return new ProguardMapId(hasher.hash().toString().substring(0, PG_MAP_ID_LENGTH));
+  private ProguardMapId computeProguardMapId() {
+    ProguardMapIdBuilder builder = new ProguardMapIdBuilder();
+    classNameMapper.write(builder);
+    return builder.build();
   }
 
-  private void writeBody(StringBuilder builder, String body) {
-    builder.append(body);
+  private void writeBody() {
+    classNameMapper.write(new ProguardMapWriter());
   }
 
-  private void writeMarker(StringBuilder builder, ProguardMapId id) {
+  private void writeMarker(ProguardMapId id) {
+    StringBuilder builder = new StringBuilder();
     builder.append(
         "# "
             + MARKER_KEY_COMPILER
@@ -94,15 +101,71 @@
     builder.append("# " + MARKER_KEY_PG_MAP_ID + ": " + id.get() + "\n");
     // Turn off linting of the mapping file in some build systems.
     builder.append("# common_typos_disable" + "\n");
+    consumer.accept(builder.toString(), reporter);
   }
 
-  private static boolean validateProguardMapParses(String content) {
-    try {
-      ClassNameMapper.mapperFromString(content);
-    } catch (IOException e) {
-      e.printStackTrace();
-      return false;
+  static class ProguardMapIdBuilder implements ChainableStringConsumer {
+
+    private final Hasher hasher = Hashing.murmur3_32().newHasher();
+
+    @Override
+    public ProguardMapIdBuilder accept(String string) {
+      for (int i = 0; i < string.length(); i++) {
+        char c = string.charAt(i);
+        if (!Character.isWhitespace(c)) {
+          hasher.putInt(c);
+        }
+      }
+      return this;
     }
-    return true;
+
+    public ProguardMapId build() {
+      return new ProguardMapId(hasher.hash().toString().substring(0, PG_MAP_ID_LENGTH));
+    }
+  }
+
+  class ProguardMapWriter implements ChainableStringConsumer {
+
+    @Override
+    public ProguardMapWriter accept(String string) {
+      consumer.accept(string, reporter);
+      return this;
+    }
+  }
+
+  static class ProguardMapChecker implements StringConsumer {
+
+    private final StringConsumer inner;
+    private final StringBuilder contents = new StringBuilder();
+
+    ProguardMapChecker(StringConsumer inner) {
+      if (!InternalOptions.assertionsEnabled()) {
+        // Make sure we never get here without assertions enabled.
+        throw new Unreachable();
+      }
+      this.inner = inner;
+    }
+
+    @Override
+    public void accept(String string, DiagnosticsHandler handler) {
+      inner.accept(string, handler);
+      contents.append(string);
+    }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {
+      inner.finished(handler);
+      assert validateProguardMapParses(contents.toString());
+    }
+
+    private static boolean validateProguardMapParses(String content) {
+      try {
+        ClassNameMapper.mapperFromString(content);
+      } catch (IOException e) {
+        e.printStackTrace();
+        return false;
+      }
+      return true;
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/ChainableConsumer.java b/src/main/java/com/android/tools/r8/utils/ChainableConsumer.java
new file mode 100644
index 0000000..7d69754
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/ChainableConsumer.java
@@ -0,0 +1,10 @@
+// Copyright (c) 2020, 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.utils;
+
+public interface ChainableConsumer<T> {
+
+  ChainableConsumer<T> accept(T value);
+}
diff --git a/src/main/java/com/android/tools/r8/utils/ChainableStringConsumer.java b/src/main/java/com/android/tools/r8/utils/ChainableStringConsumer.java
new file mode 100644
index 0000000..d1636c5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/ChainableStringConsumer.java
@@ -0,0 +1,23 @@
+// Copyright (c) 2020, 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.utils;
+
+import java.util.function.Consumer;
+
+public interface ChainableStringConsumer extends ChainableConsumer<String> {
+
+  @Override
+  ChainableStringConsumer accept(String string);
+
+  static ChainableStringConsumer wrap(Consumer<String> consumer) {
+    return new ChainableStringConsumer() {
+      @Override
+      public ChainableStringConsumer accept(String value) {
+        consumer.accept(value);
+        return this;
+      }
+    };
+  }
+}