Add support for additional mapping information in proguard maps

This CL adds everything needed for adding information to mapping files
and having it parsed out again when reading signatures. It is tested
by hand-written tests and therefore the design may change when we
start to produce additional information.

Bug: 140530010
Bug: 116081360
Change-Id: Ic5745e50a164b3edae31fd037c68f865afc8bcd5
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 e98d9b9..8e6d95c 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.position.Position;
 import com.android.tools.r8.utils.BiMapContainer;
 import com.android.tools.r8.utils.ChainableStringConsumer;
+import com.android.tools.r8.utils.Reporter;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.ImmutableBiMap;
 import com.google.common.collect.ImmutableMap;
@@ -68,7 +69,7 @@
 
   public static ClassNameMapper mapperFromInputStream(InputStream in) throws IOException {
     return mapperFromBufferedReader(
-        new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)));
+        new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)), null);
   }
 
   public static ClassNameMapper mapperFromFile(Path path) throws IOException {
@@ -87,12 +88,18 @@
   }
 
   public static ClassNameMapper mapperFromString(String contents) throws IOException {
-    return mapperFromBufferedReader(CharSource.wrap(contents).openBufferedStream());
+    return mapperFromBufferedReader(CharSource.wrap(contents).openBufferedStream(), null);
   }
 
-  private static ClassNameMapper mapperFromBufferedReader(BufferedReader reader)
+  public static ClassNameMapper mapperFromString(String contents, Reporter reporter)
       throws IOException {
-    try (ProguardMapReader proguardReader = new ProguardMapReader(reader)) {
+    return mapperFromBufferedReader(CharSource.wrap(contents).openBufferedStream(), reporter);
+  }
+
+  private static ClassNameMapper mapperFromBufferedReader(BufferedReader reader, Reporter reporter)
+      throws IOException {
+    try (ProguardMapReader proguardReader =
+        new ProguardMapReader(reader, reporter != null ? reporter : new Reporter())) {
       ClassNameMapper.Builder builder = ClassNameMapper.builder();
       proguardReader.parse(builder);
       return builder.build();
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 5a00fcb..ae1b6d0 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNamingForNameMapper.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNamingForNameMapper.java
@@ -43,9 +43,9 @@
     @Override
     public ClassNaming.Builder addMemberEntry(MemberNaming entry) {
       if (entry.isMethodNaming()) {
-        methodMembers.put((MethodSignature) entry.getRenamedSignature(), entry);
+        methodMembers.put(entry.getRenamedSignature().asMethodSignature(), entry);
       } else {
-        fieldMembers.put((FieldSignature) entry.getRenamedSignature(), entry);
+        fieldMembers.put(entry.getRenamedSignature().asFieldSignature(), entry);
       }
       mappedNamingsByName
           .computeIfAbsent(entry.getRenamedName(), m -> new ArrayList<>())
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 84f726d..bf0a8dd 100644
--- a/src/main/java/com/android/tools/r8/naming/MemberNaming.java
+++ b/src/main/java/com/android/tools/r8/naming/MemberNaming.java
@@ -58,12 +58,12 @@
   final Position position;
 
   public MemberNaming(Signature signature, String renamedName) {
-    this(signature, renamedName, Position.UNKNOWN);
+    this(signature, signature.asRenamed(renamedName), Position.UNKNOWN);
   }
 
-  public MemberNaming(Signature signature, String renamedName, Position position) {
+  public MemberNaming(Signature signature, Signature renamedSignature, Position position) {
     this.signature = signature;
-    this.renamedSignature = signature.asRenamed(renamedName);
+    this.renamedSignature = renamedSignature;
     this.position = position;
   }
 
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java b/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
index 747423f..2a67342 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
@@ -6,14 +6,21 @@
 import com.android.tools.r8.naming.MemberNaming.FieldSignature;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.naming.MemberNaming.Signature;
+import com.android.tools.r8.naming.mappinginformation.MappingInformation;
+import com.android.tools.r8.naming.mappinginformation.SignatureMappingInformation;
 import com.android.tools.r8.position.TextPosition;
 import com.android.tools.r8.utils.IdentifierUtils;
+import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.Maps;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 
 /**
@@ -54,6 +61,8 @@
 public class ProguardMapReader implements AutoCloseable {
 
   private final BufferedReader reader;
+  private final JsonParser jsonParser = new JsonParser();
+  private final Reporter reporter;
 
   @Override
   public void close() throws IOException {
@@ -62,8 +71,11 @@
     }
   }
 
-  ProguardMapReader(BufferedReader reader) {
+  ProguardMapReader(BufferedReader reader, Reporter reporter) {
     this.reader = reader;
+    this.reporter = reporter;
+    assert reader != null;
+    assert reporter != null;
   }
 
   // Internal parser state
@@ -118,7 +130,7 @@
     for (int i = 0; i < line.length(); ++i) {
       char c = line.charAt(i);
       if (c == '#') {
-        return true;
+        return !hasFirstCharJsonBrace(line, i);
       } else if (!StringUtils.isWhitespace(c)) {
         return false;
       }
@@ -126,6 +138,33 @@
     return true;
   }
 
+  private boolean isCommentLineWithJsonBrace() {
+    if (line == null) {
+      return false;
+    }
+    for (int i = 0; i < line.length(); ++i) {
+      char c = line.charAt(i);
+      if (c == '#') {
+        return hasFirstCharJsonBrace(line, i);
+      } else if (!Character.isWhitespace(c)) {
+        return false;
+      }
+    }
+    return false;
+  }
+
+  private static boolean hasFirstCharJsonBrace(String line, int commentCharIndex) {
+    for (int i = commentCharIndex + 1; i < line.length(); i++) {
+      char c = line.charAt(i);
+      if (c == '{') {
+        return true;
+      } else if (!Character.isWhitespace(c)) {
+        return false;
+      }
+    }
+    return false;
+  }
+
   private boolean skipLine() throws IOException {
     lineOffset = 0;
     do {
@@ -213,10 +252,22 @@
     MemberNaming lastAddedNaming = null;
     MemberNaming activeMemberNaming = null;
     Range previousMappedRange = null;
+    Map<Signature, SignatureMappingInformation> mappingInformation = Maps.newHashMap();
     do {
       Object originalRange = null;
       Range mappedRange = null;
-
+      // Try to parse any information added in comments above member namings
+      if (isCommentLineWithJsonBrace()) {
+        MappingInformation mappingInfo =
+            MappingInformation.fromJsonObject(parseJsonInComment(), reporter, lineNo);
+        if (mappingInfo != null && mappingInfo.isSignatureMappingInformation()) {
+          SignatureMappingInformation sigMapInfo = mappingInfo.asSignatureMappingInformation();
+          mappingInformation.put(sigMapInfo.getSignature(), sigMapInfo);
+        }
+        // Skip reading the rest of the line.
+        lineOffset = line.length();
+        continue;
+      }
       // Parse the member line '  x:y:name:z:q -> renamedName'.
       if (!StringUtils.isWhitespace(peekCodePoint())) {
         break;
@@ -274,7 +325,16 @@
           }
         }
       }
-      activeMemberNaming = new MemberNaming(signature, renamedName, getPosition());
+      if (mappingInformation.containsKey(signature)) {
+        activeMemberNaming =
+            new MemberNaming(
+                signature,
+                mappingInformation.get(signature).apply(signature, renamedName, reporter),
+                getPosition());
+      } else {
+        activeMemberNaming =
+            new MemberNaming(signature, signature.asRenamed(renamedName), getPosition());
+      }
       previousMappedRange = mappedRange;
     } while (nextLine());
 
@@ -451,6 +511,20 @@
     return result;
   }
 
+  private JsonObject parseJsonInComment() {
+    assert isCommentLineWithJsonBrace();
+    try {
+      int firstIndex = 0;
+      while (line.charAt(firstIndex) != '{') {
+        firstIndex++;
+      }
+      return jsonParser.parse(line.substring(firstIndex)).getAsJsonObject();
+    } catch (com.google.gson.JsonSyntaxException ex) {
+      // An info message is reported in MappingInformation.
+      return null;
+    }
+  }
+
   private class ParseException extends RuntimeException {
 
     private final int lineNo;
diff --git a/src/main/java/com/android/tools/r8/naming/SeedMapper.java b/src/main/java/com/android/tools/r8/naming/SeedMapper.java
index be69416..5c50c06 100644
--- a/src/main/java/com/android/tools/r8/naming/SeedMapper.java
+++ b/src/main/java/com/android/tools/r8/naming/SeedMapper.java
@@ -74,7 +74,7 @@
   private static SeedMapper seedMapperFromInputStream(Reporter reporter, InputStream in)
       throws IOException {
     BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
-    try (ProguardMapReader proguardReader = new ProguardMapReader(reader)) {
+    try (ProguardMapReader proguardReader = new ProguardMapReader(reader, reporter)) {
       SeedMapper.Builder builder = SeedMapper.builder(reporter);
       proguardReader.parse(builder);
       return builder.build();
diff --git a/src/main/java/com/android/tools/r8/naming/mappinginformation/InformationParsingError.java b/src/main/java/com/android/tools/r8/naming/mappinginformation/InformationParsingError.java
new file mode 100644
index 0000000..00e6dc5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/naming/mappinginformation/InformationParsingError.java
@@ -0,0 +1,95 @@
+// Copyright (c) 2019, 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.mappinginformation;
+
+import com.android.tools.r8.Diagnostic;
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+import com.android.tools.r8.position.TextPosition;
+
+@Keep
+public class InformationParsingError implements Diagnostic {
+
+  private final String message;
+  private final Position position;
+
+  @Override
+  public Origin getOrigin() {
+    return Origin.unknown();
+  }
+
+  @Override
+  public Position getPosition() {
+    return position;
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return message;
+  }
+
+  private InformationParsingError(String message, Position position) {
+    this.message = message;
+    this.position = position;
+  }
+
+  static InformationParsingError noHandlerFor(int lineNumber, String value) {
+    return new InformationParsingError(
+        String.format("Could not find a handler for %s", value),
+        new TextPosition(1, lineNumber, TextPosition.UNKNOWN_COLUMN));
+  }
+
+  static InformationParsingError noKeyInJson(int lineNumber, String key) {
+    return new InformationParsingError(
+        String.format("Could not locate '%s' in the JSON object", key),
+        new TextPosition(1, lineNumber, TextPosition.UNKNOWN_COLUMN));
+  }
+
+  static InformationParsingError notValidJson(int lineNumber) {
+    return new InformationParsingError(
+        "Not valid JSON", new TextPosition(1, lineNumber, TextPosition.UNKNOWN_COLUMN));
+  }
+
+  static InformationParsingError notValidString(int lineNumber, String key) {
+    return new InformationParsingError(
+        String.format("The value of '%s' is not a valid string in the JSON object", key),
+        new TextPosition(1, lineNumber, TextPosition.UNKNOWN_COLUMN));
+  }
+
+  static InformationParsingError tooManyInformationalParameters(int lineNumber) {
+    return new InformationParsingError(
+        "More informational parameters than actual parameters for method signature",
+        new TextPosition(1, lineNumber, TextPosition.UNKNOWN_COLUMN));
+  }
+
+  static InformationParsingError noKeyForObjectWithId(
+      int lineNumber, String key, String mappingKey, String mappingValue) {
+    return new InformationParsingError(
+        String.format("Could not find '%s' for object with %s '%s'", key, mappingKey, mappingValue),
+        new TextPosition(1, lineNumber, TextPosition.UNKNOWN_COLUMN));
+  }
+
+  static InformationParsingError invalidValueForObjectWithId(
+      int lineNumber, String mappingKey, String mappingValue) {
+    return new InformationParsingError(
+        String.format(
+            "Could not decode the information for the object with %s '%s'",
+            mappingKey, mappingValue),
+        new TextPosition(1, lineNumber, TextPosition.UNKNOWN_COLUMN));
+  }
+
+  static InformationParsingError tooManyEntriesForParameterInformation(int lineNumber) {
+    return new InformationParsingError(
+        "Parameter information do not have 1 or 2 entries",
+        new TextPosition(1, lineNumber, TextPosition.UNKNOWN_COLUMN));
+  }
+
+  static InformationParsingError invalidParameterInformationObject(int lineNumber) {
+    return new InformationParsingError(
+        "Parameter information is not an index and a string representation of a type",
+        new TextPosition(1, lineNumber, TextPosition.UNKNOWN_COLUMN));
+  }
+}
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
new file mode 100644
index 0000000..b471629
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/naming/mappinginformation/MappingInformation.java
@@ -0,0 +1,67 @@
+// Copyright (c) 2019, 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.mappinginformation;
+
+import com.android.tools.r8.utils.Reporter;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+public abstract class MappingInformation {
+
+  public static final String MAPPING_ID_KEY = "id";
+
+  private final int lineNumber;
+
+  MappingInformation(int lineNumber) {
+    this.lineNumber = lineNumber;
+  }
+
+  public int getLineNumber() {
+    return lineNumber;
+  }
+
+  public abstract String serialize();
+
+  public boolean isSignatureMappingInformation() {
+    return false;
+  }
+
+  public SignatureMappingInformation asSignatureMappingInformation() {
+    return null;
+  }
+
+  public static MappingInformation fromJsonObject(
+      JsonObject object, Reporter reporter, int lineNumber) {
+    if (object == null) {
+      reporter.info(InformationParsingError.notValidJson(lineNumber));
+      return null;
+    }
+    JsonElement id = object.get(MAPPING_ID_KEY);
+    if (id == null) {
+      reporter.info(InformationParsingError.noKeyInJson(lineNumber, MAPPING_ID_KEY));
+      return null;
+    }
+    String idString = id.getAsString();
+    if (idString == null) {
+      reporter.info(InformationParsingError.notValidString(lineNumber, MAPPING_ID_KEY));
+      return null;
+    }
+    if (idString.equals(MethodSignatureChangedInformation.ID)) {
+      return MethodSignatureChangedInformation.build(object, reporter, lineNumber);
+    }
+    reporter.info(InformationParsingError.noHandlerFor(lineNumber, idString));
+    return null;
+  }
+
+  static JsonElement getJsonElementFromObject(
+      JsonObject object, Reporter reporter, int lineNumber, String key, String id) {
+    JsonElement element = object.get(key);
+    if (element == null) {
+      reporter.info(
+          InformationParsingError.noKeyForObjectWithId(lineNumber, key, MAPPING_ID_KEY, id));
+    }
+    return element;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/naming/mappinginformation/MethodSignatureChangedInformation.java b/src/main/java/com/android/tools/r8/naming/mappinginformation/MethodSignatureChangedInformation.java
new file mode 100644
index 0000000..32978ae
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/naming/mappinginformation/MethodSignatureChangedInformation.java
@@ -0,0 +1,247 @@
+// Copyright (c) 2019, 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.mappinginformation;
+
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.naming.MemberNaming.Signature;
+import com.android.tools.r8.utils.Reporter;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+
+/**
+ * The MethodSignatureChangedInformation structure adds extra information regarding the mapped
+ * method signature that is otherwise not available in the existing proguard mapping format. The
+ * JSON-structure is as follows:
+ *
+ * <pre>
+ *   {
+ *     "id": "argumentsChanged",
+ *     "signature": { methodSignature },
+ *     "returnType": "java.lang.String",
+ *     "receiver": false,
+ *     "params": [
+ *       [1], // <-- parameter with original index 1 (starting index based on receiver) is removed.
+ *       [2, Foo] // <-- parameter with index 2 has type Foo
+ *     ]
+ *   }
+ * </pre>
+ */
+public class MethodSignatureChangedInformation extends SignatureMappingInformation {
+
+  private ParameterInformation[] argumentInfos;
+  private final boolean receiver;
+  private final String returnType;
+  private final MethodSignature signature;
+
+  public static final String ID = "methodSignatureChanged";
+  private static final String RETURN_TYPE_KEY = "returnType";
+  private static final String PARAMS_KEY = "params";
+  private static final String RECEIVER_KEY = "receiver";
+
+  @Override
+  public String serialize() {
+    JsonObject result = new JsonObject();
+    serializeMethodSignature(result, signature);
+    result.add(MAPPING_ID_KEY, new JsonPrimitive(ID));
+    result.add(RECEIVER_KEY, new JsonPrimitive(receiver));
+    result.add(RETURN_TYPE_KEY, new JsonPrimitive(returnType));
+    JsonArray arguments = new JsonArray();
+    for (ParameterInformation argInfo : argumentInfos) {
+      arguments.add(argInfo.serialize());
+    }
+    result.add(PARAMS_KEY, arguments);
+    return result.toString();
+  }
+
+  @Override
+  public Signature getSignature() {
+    return signature;
+  }
+
+  @Override
+  public Signature apply(Signature originalSignature, String renamedName, Reporter reporter) {
+    if (originalSignature == null || !originalSignature.isMethodSignature()) {
+      assert false : "Should only call apply for method signature";
+      return originalSignature;
+    }
+    MethodSignature signature = originalSignature.asMethodSignature();
+    String type = signature.type;
+    String[] parameters = signature.parameters;
+    int numberOfArgumentsRemoved = getNumberOfArgumentsRemoved();
+    if (numberOfArgumentsRemoved > parameters.length) {
+      // The mapping information is not up to date with the current signature.
+      reporter.warning(InformationParsingError.tooManyInformationalParameters(getLineNumber()));
+      return new MethodSignature(renamedName, type, parameters);
+    }
+    String[] newParameters = new String[parameters.length - numberOfArgumentsRemoved];
+    int insertIndex = 0;
+    for (int i = 0; i < parameters.length; i++) {
+      ParameterInformation argInfo = getParameterInformation(i);
+      if (argInfo != null && argInfo.getType() == null) {
+        // Argument has been removed.
+      } else {
+        if (insertIndex >= newParameters.length) {
+          // The mapping information is not up to date with the current signature.
+          reporter.warning(InformationParsingError.tooManyInformationalParameters(getLineNumber()));
+          return new MethodSignature(renamedName, type, parameters);
+        } else if (argInfo == null) {
+          // Unchanged, take current parameter.
+          newParameters[insertIndex++] = parameters[i];
+        } else {
+          newParameters[insertIndex++] = argInfo.getType();
+        }
+      }
+    }
+    assert insertIndex == newParameters.length;
+    return new MethodSignature(renamedName, getReturnType(), newParameters);
+  }
+
+  @Override
+  public boolean hasChangedArguments() {
+    return true;
+  }
+
+  public int getNumberOfArgumentsRemoved() {
+    int removedCount = 0;
+    for (ParameterInformation argInfo : argumentInfos) {
+      if (argInfo.type == null) {
+        removedCount++;
+      }
+    }
+    return removedCount;
+  }
+
+  public boolean hasReceiver() {
+    return receiver;
+  }
+
+  public String getReturnType() {
+    return returnType;
+  }
+
+  public ParameterInformation getParameterInformation(int index) {
+    int subtractIndex = receiver ? 1 : 0;
+    for (int i = 0; i < argumentInfos.length; i++) {
+      if (argumentInfos[i].index - subtractIndex == index) {
+        return argumentInfos[i];
+      }
+    }
+    return null;
+  }
+
+  @Override
+  public MethodSignatureChangedInformation asArgumentsChangedInformation() {
+    return this;
+  }
+
+  private MethodSignatureChangedInformation(
+      MethodSignature signature,
+      String returnType,
+      boolean hasReceiver,
+      ParameterInformation[] argumentInfos,
+      int lineNumber) {
+    super(lineNumber);
+    this.signature = signature;
+    this.argumentInfos = argumentInfos;
+    this.returnType = returnType;
+    this.receiver = hasReceiver;
+  }
+
+  public static MappingInformation build(JsonObject object, Reporter reporter, int lineNumber) {
+    try {
+      JsonElement returnTypeElement =
+          getJsonElementFromObject(object, reporter, lineNumber, RETURN_TYPE_KEY, ID);
+      JsonElement receiverElement =
+          getJsonElementFromObject(object, reporter, lineNumber, RECEIVER_KEY, ID);
+      JsonElement argsElement =
+          getJsonElementFromObject(object, reporter, lineNumber, PARAMS_KEY, ID);
+      MethodSignature signature = getMethodSignature(object, ID, reporter, lineNumber);
+      if (signature == null
+          || returnTypeElement == null
+          || receiverElement == null
+          || argsElement == null) {
+        return null;
+      }
+      JsonArray argumentsArray = argsElement.getAsJsonArray();
+      if (argumentsArray == null) {
+        return null;
+      }
+      ParameterInformation[] args = new ParameterInformation[argumentsArray.size()];
+      for (int i = 0; i < argumentsArray.size(); i++) {
+        args[i] =
+            ParameterInformation.fromJsonArray(
+                argumentsArray.get(i).getAsJsonArray(), reporter, lineNumber);
+      }
+      return new MethodSignatureChangedInformation(
+          signature,
+          returnTypeElement.getAsString(),
+          receiverElement.getAsBoolean(),
+          args,
+          lineNumber);
+    } catch (UnsupportedOperationException | IllegalStateException ignored) {
+      reporter.info(
+          InformationParsingError.invalidValueForObjectWithId(lineNumber, MAPPING_ID_KEY, ID));
+      return null;
+    }
+  }
+
+  public static class ParameterInformation {
+    private final int index;
+    private final String type;
+
+    public int getIndex() {
+      return index;
+    }
+
+    public String getType() {
+      return type;
+    }
+
+    private ParameterInformation(int index, String type) {
+      this.index = index;
+      this.type = type;
+    }
+
+    static ParameterInformation fromJsonArray(
+        JsonArray argumentInfo, Reporter reporter, int lineNumber) {
+      assert argumentInfo != null;
+      try {
+        if (argumentInfo.size() > 2) {
+          reporter.info(InformationParsingError.tooManyEntriesForParameterInformation(lineNumber));
+          return null;
+        }
+        int index = argumentInfo.get(0).getAsInt();
+        if (argumentInfo.size() == 1) {
+          // This is a removed argument - no type information
+          return new ParameterInformation(index, null);
+        } else {
+          return new ParameterInformation(index, argumentInfo.get(1).getAsString());
+        }
+      } catch (UnsupportedOperationException | IllegalStateException ignored) {
+        reporter.info(InformationParsingError.invalidParameterInformationObject(lineNumber));
+        return null;
+      }
+    }
+
+    public static ParameterInformation buildRemovedParameterInformation(int index) {
+      return new ParameterInformation(index, null);
+    }
+
+    public static ParameterInformation buildChangedParameterInformation(int index, String type) {
+      return new ParameterInformation(index, type);
+    }
+
+    JsonArray serialize() {
+      JsonArray serializedArray = new JsonArray();
+      serializedArray.add(index);
+      if (type != null) {
+        serializedArray.add(type);
+      }
+      return serializedArray;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/naming/mappinginformation/SignatureMappingInformation.java b/src/main/java/com/android/tools/r8/naming/mappinginformation/SignatureMappingInformation.java
new file mode 100644
index 0000000..3659a6f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/naming/mappinginformation/SignatureMappingInformation.java
@@ -0,0 +1,72 @@
+// 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.naming.mappinginformation;
+
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.naming.MemberNaming.Signature;
+import com.android.tools.r8.utils.Reporter;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+public abstract class SignatureMappingInformation extends MappingInformation {
+
+  private static final String SIGNATURE_KEY = "signature";
+
+  SignatureMappingInformation(int lineNumber) {
+    super(lineNumber);
+  }
+
+  @Override
+  public boolean isSignatureMappingInformation() {
+    return true;
+  }
+
+  @Override
+  public SignatureMappingInformation asSignatureMappingInformation() {
+    return this;
+  }
+
+  public abstract Signature getSignature();
+
+  public boolean hasChangedArguments() {
+    return false;
+  }
+
+  public MethodSignatureChangedInformation asArgumentsChangedInformation() {
+    return null;
+  }
+
+  public abstract Signature apply(
+      Signature originalSignature, String renamedName, Reporter reporter);
+
+  JsonObject serializeMethodSignature(JsonObject object, MethodSignature signature) {
+    JsonArray signatureArr = new JsonArray();
+    signatureArr.add(signature.type);
+    signatureArr.add(signature.name);
+    for (String parameter : signature.parameters) {
+      signatureArr.add(parameter);
+    }
+    object.add(SIGNATURE_KEY, signatureArr);
+    return object;
+  }
+
+  static MethodSignature getMethodSignature(
+      JsonObject object, String id, Reporter reporter, int lineNumber) {
+    JsonElement signatureElement =
+        getJsonElementFromObject(object, reporter, lineNumber, SIGNATURE_KEY, id);
+    if (signatureElement == null || !signatureElement.isJsonArray()) {
+      return null;
+    }
+    // Signature will be [returnType, name, param1, param2, ...].
+    JsonArray signature = signatureElement.getAsJsonArray();
+    String[] parameters = new String[signature.size() - 2];
+    for (int i = 2; i < signature.size(); i++) {
+      parameters[i - 2] = signature.get(i).getAsString();
+    }
+    return new MethodSignature(
+        signature.get(1).getAsString(), signature.get(0).getAsString(), parameters);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/ProguardMapReaderArgumentsTest.java b/src/test/java/com/android/tools/r8/naming/ProguardMapReaderArgumentsTest.java
new file mode 100644
index 0000000..2965085
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/ProguardMapReaderArgumentsTest.java
@@ -0,0 +1,226 @@
+// Copyright (c) 2019, 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.DiagnosticsMatcher.diagnosticMessage;
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticPosition;
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
+import static com.android.tools.r8.PositionMatcher.positionLine;
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertFalse;
+import static junit.framework.TestCase.assertNotNull;
+import static junit.framework.TestCase.assertTrue;
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.core.StringContains.containsString;
+
+import com.android.tools.r8.TestDiagnosticMessagesImpl;
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.naming.mappinginformation.InformationParsingError;
+import com.android.tools.r8.utils.Reporter;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ProguardMapReaderArgumentsTest {
+
+  private Reporter reporter;
+  private TestDiagnosticMessagesImpl testDiagnosticMessages;
+
+  @Before
+  public void setUp() {
+    testDiagnosticMessages = new TestDiagnosticMessagesImpl();
+    reporter = new Reporter(testDiagnosticMessages);
+  }
+
+  @Test
+  public void testMethodCanParseMemberComments() throws IOException {
+    String mapWithArgumentRemovalInformation =
+        StringUtils.join(
+            "\n",
+            "android.constraint.Placeholder -> a.b.b.f:",
+            "# Just a comment", // Regular comment
+            "    int mContentId -> b",
+            "# {}", // Valid JSON with no information
+            "    147:161:void updatePreLayout(android.constraint.Layout) -> a",
+            "# {foo:bar}}}", // Not valid JSON
+            "    194:204:void updatePostMeasure(android.constraint.Layout) -> b",
+            "# { 'id': 'bar' }", // Valid json but no handler for bar.
+            "    194:204:void updatePostMeasure(android.constraint.Layout) -> c");
+    ClassNameMapper cnm =
+        ClassNameMapper.mapperFromString(mapWithArgumentRemovalInformation, reporter);
+    ClassNamingForNameMapper classNaming = cnm.getClassNaming("a.b.b.f");
+    assertNotNull(classNaming);
+    testDiagnosticMessages.assertOnlyInfos();
+    testDiagnosticMessages
+        .assertInfosMatch(
+            ImmutableList.of(
+                allOf(
+                    diagnosticMessage(containsString("Could not locate 'id'")),
+                    diagnosticPosition(positionLine(4))),
+                allOf(
+                    diagnosticMessage(containsString("Not valid JSON")),
+                    diagnosticPosition(positionLine(6))),
+                allOf(
+                    diagnosticMessage(containsString("Could not find a handler for bar")),
+                    diagnosticPosition(positionLine(8)))))
+        .assertAllInfosMatch(diagnosticType(InformationParsingError.class));
+  }
+
+  @Test
+  public void testMethodWillReportWhenParsingArgumentsChangedMemberComments() throws IOException {
+    String mapWithArgumentRemovalInformation =
+        StringUtils.join(
+                "\n",
+                "android.constraint.Placeholder -> a.b.b.f:",
+                "# Just a comment", // Regular comment
+                "    int mContentId -> b",
+                // Valid JSON but missing signature
+                "# { 'id': 'methodSignatureChanged', "
+                    + "'signature': ['void', 'updatePreLayout', 'android.constraint.Layout'],"
+                    + "'returnType': [], 'receiver': true, 'params': []}",
+                "    147:161:void updatePreLayout(android.constraint.Layout) -> a",
+                // Valid JSON but not an argumentsChanged object
+                "# { 'id': 'methodSignatureChanged', "
+                    + "'signature': ['void', 'updatePreLayout', 'android.constraint.Layout'],"
+                    + "'returnType': [], 'receiver': true, 'params': []}",
+                "    147:161:void updatePreLayout(android.constraint.Layout) -> a",
+                // Valid JSON but not an arguments_changed object.
+                "# { 'id': 'methodSignatureChanged', "
+                    + "'signature': ['void','updatePreLayout','android.constraint.Layout'],"
+                    + "'returnType': 'foo', 'receiver': 1, 'params': 'foo' }",
+                "    194:204:void updatePostMeasure(android.constraint.Layout) -> a")
+            .replace("'", "\"");
+    ClassNameMapper cnm =
+        ClassNameMapper.mapperFromString(mapWithArgumentRemovalInformation, reporter);
+    ClassNamingForNameMapper classNaming = cnm.getClassNaming("a.b.b.f");
+    assertNotNull(classNaming);
+    testDiagnosticMessages.assertOnlyInfos();
+    testDiagnosticMessages
+        .assertInfosMatch(
+            ImmutableList.of(
+                allOf(
+                    diagnosticMessage(containsString("Could not decode")),
+                    diagnosticPosition(positionLine(4))),
+                allOf(
+                    diagnosticMessage(containsString("Could not decode")),
+                    diagnosticPosition(positionLine(6))),
+                allOf(
+                    diagnosticMessage(containsString("Could not decode")),
+                    diagnosticPosition(positionLine(8)))))
+        .assertAllInfosMatch(diagnosticType(InformationParsingError.class));
+  }
+
+  @Test
+  public void testMethodCanParseArgumentRemoval() throws IOException {
+    String mapWithArgumentRemovalInformation =
+        StringUtils.lines(
+            "android.constraint.Placeholder -> a.b.b.f:",
+            "    int mContentId -> b",
+            "# { 'id': 'methodSignatureChanged',"
+                + "'signature': ["
+                + "'void','updatePreLayout','android.constraint.Layout','String','int'],"
+                + " 'returnType': 'void', 'receiver': true, 'params':[[1],[2]]}",
+            "    147:161:void updatePreLayout(android.constraint.Layout,String,int) -> a",
+            "# {'id':'methodSignatureChanged',"
+                + "'signature': ["
+                + "'void','updatePreMeasure','android.constraint.Layout','String','int'],"
+                + "'returnType': 'void', 'receiver': true, 'params':[[3]]}",
+            "    162:173:void updatePreMeasure(android.constraint.Layout,String,int) -> a",
+            "    194:204:void updatePostMeasure(android.constraint.Layout,String,int) -> a");
+    ClassNameMapper cnm = ClassNameMapper.mapperFromString(mapWithArgumentRemovalInformation);
+    ClassNamingForNameMapper classNaming = cnm.getClassNaming("a.b.b.f");
+    assertNotNull(classNaming);
+
+    List<MemberNaming> members = classNaming.lookupByOriginalName("mContentId");
+    assertFalse(members.isEmpty());
+    MemberNaming fieldContentId = members.get(0);
+    assertNotNull(fieldContentId);
+    assertTrue(!fieldContentId.isMethodNaming());
+
+    members = classNaming.lookupByOriginalName("updatePreLayout");
+    assertFalse(members.isEmpty());
+    MemberNaming updatePreLayout = members.get(0);
+    assertNotNull(updatePreLayout);
+    assertTrue(updatePreLayout.isMethodNaming());
+    MethodSignature renamedPreLayout = (MethodSignature) updatePreLayout.getRenamedSignature();
+    assertEquals(1, renamedPreLayout.parameters.length);
+    assertEquals("int", renamedPreLayout.parameters[0]);
+
+    members = classNaming.lookupByOriginalName("updatePreMeasure");
+    assertFalse(members.isEmpty());
+    MemberNaming updatePreMeasure = members.get(0);
+    assertNotNull(updatePreMeasure);
+    assertTrue(updatePreMeasure.isMethodNaming());
+    MethodSignature renamedPreMeasure = (MethodSignature) updatePreMeasure.getRenamedSignature();
+    assertEquals(2, renamedPreMeasure.parameters.length);
+    assertEquals("android.constraint.Layout", renamedPreMeasure.parameters[0]);
+    assertEquals("String", renamedPreMeasure.parameters[1]);
+
+    members = classNaming.lookupByOriginalName("updatePostMeasure");
+    assertFalse(members.isEmpty());
+    MemberNaming updatePostMeasure = members.get(0);
+    assertNotNull(updatePostMeasure);
+    assertTrue(updatePostMeasure.isMethodNaming());
+    MethodSignature renamedPostMeasure = (MethodSignature) updatePostMeasure.getRenamedSignature();
+    assertEquals(3, renamedPostMeasure.parameters.length);
+  }
+
+  @Test
+  public void testMethodCanParseArgumentChanged() throws IOException {
+    String mapWithArgumentRemovalInformation =
+        StringUtils.join(
+            "\n",
+            "android.constraint.Placeholder -> a.b.b.f:",
+            "# {'id':'methodSignatureChanged',"
+                + "'signature':["
+                + "'void','updatePreLayout','android.constraint.Layout','String','float'],"
+                + "'returnType': 'void',"
+                + "'receiver': true,"
+                + "'params':[[1,int],[2,Foo]]}",
+            "# {'id':'methodSignatureChanged',"
+                + "'signature':["
+                + "'void','updatePreMeasure','android.constraint.Layout','String','int'],"
+                + "'returnType': 'void', "
+                + "'receiver': true, "
+                + "'params':[[2,com.baz.Bar],[3]]}",
+            "  147:161:void updatePreLayout(android.constraint.Layout,String,float) -> a",
+            "  162:173:void updatePreMeasure(android.constraint.Layout,String,int) -> a",
+            "  194:204:void updatePostMeasure(android.constraint.Layout,String,int) -> a");
+    ClassNameMapper cnm = ClassNameMapper.mapperFromString(mapWithArgumentRemovalInformation);
+    ClassNamingForNameMapper classNaming = cnm.getClassNaming("a.b.b.f");
+    assertNotNull(classNaming);
+
+    List<MemberNaming> members = classNaming.lookupByOriginalName("updatePreLayout");
+    assertFalse(members.isEmpty());
+    MemberNaming updatePreLayout = members.get(0);
+    assertNotNull(updatePreLayout);
+    assertTrue(updatePreLayout.isMethodNaming());
+    MethodSignature renamedPreLayout = (MethodSignature) updatePreLayout.getRenamedSignature();
+    assertEquals(3, renamedPreLayout.parameters.length);
+    assertEquals("int", renamedPreLayout.parameters[0]);
+    assertEquals("Foo", renamedPreLayout.parameters[1]);
+    assertEquals("float", renamedPreLayout.parameters[2]);
+
+    members = classNaming.lookupByOriginalName("updatePreMeasure");
+    assertFalse(members.isEmpty());
+    MemberNaming updatePreMeasure = members.get(0);
+    assertNotNull(updatePreMeasure);
+    assertTrue(updatePreMeasure.isMethodNaming());
+    MethodSignature renamedPreMeasure = (MethodSignature) updatePreMeasure.getRenamedSignature();
+    assertEquals(2, renamedPreMeasure.parameters.length);
+    assertEquals("android.constraint.Layout", renamedPreMeasure.parameters[0]);
+    assertEquals("com.baz.Bar", renamedPreMeasure.parameters[1]);
+
+    members = classNaming.lookupByOriginalName("updatePostMeasure");
+    assertFalse(members.isEmpty());
+    MemberNaming updatePostMeasure = members.get(0);
+    assertNotNull(updatePostMeasure);
+    assertTrue(updatePostMeasure.isMethodNaming());
+    MethodSignature renamedPostMeasure = (MethodSignature) updatePostMeasure.getRenamedSignature();
+    assertEquals(3, renamedPostMeasure.parameters.length);
+  }
+}