Initial support for removing unused proto fields

Bug: 112437944
Change-Id: Ic17e1d0ec05489a69a319666290d639ee959bed3
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 7b34833..be25c67 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -693,12 +693,27 @@
         }
       }
 
-      {
-        MainDexClasses finalMainDexClasses = mainDexClasses;
+      if (appView.options().isProtoShrinkingEnabled()) {
+        // TODO(b/112437944): IRConverter.<init>() asserts that liveness information is available in
+        //  AppView. This is not the case if both shrinking and minification is disabled, because we
+        //  skip the second round of tree shaking in that case.
+        IRConverter converter = new IRConverter(appView, timing, null, mainDexClasses);
+
+        // If proto shrinking is enabled, we need to reprocess every dynamicMethod(). This ensures
+        // that proto fields that have been removed by the second round of tree shaking are also
+        // removed from the proto schemas in the bytecode.
+        // TODO(b/112437944): Avoid iterating the entire application to post-process every
+        //  dynamicMethod() method.
+        appView.withGeneratedMessageLiteShrinker(
+            shrinker -> shrinker.postOptimizeDynamicMethods(converter));
+
+        // If proto shrinking is enabled, we need to post-process every findLiteExtensionByNumber()
+        // method. This ensures that there are no references to dead extensions that have been
+        // removed by the second round of tree shaking.
+        // TODO(b/112437944): Avoid iterating the entire application to post-process every
+        //  findLiteExtensionByNumber() method.
         appView.withGeneratedExtensionRegistryShrinker(
-            shrinker ->
-                shrinker.postOptimizeGeneratedExtensionRegistry(
-                    new IRConverter(appView, timing, null, finalMainDexClasses)));
+            shrinker -> shrinker.postOptimizeGeneratedExtensionRegistry(converter));
       }
 
       // Add automatic main dex classes to an eventual manual list of classes.
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index d0cbbbe..00602a7 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -526,17 +526,6 @@
     return true;
   }
 
-  public DexEncodedField[] allFieldsSorted() {
-    int iLen = instanceFields.length;
-    int sLen = staticFields.length;
-    DexEncodedField[] result = new DexEncodedField[iLen + sLen];
-    System.arraycopy(instanceFields, 0, result, 0, iLen);
-    System.arraycopy(staticFields, 0, result, iLen, sLen);
-    Arrays.sort(result,
-        (DexEncodedField a, DexEncodedField b) -> a.field.slowCompareTo(b.field));
-    return result;
-  }
-
   /** Find static field in this class matching {@param field}. */
   public DexEncodedField lookupStaticField(DexField field) {
     return lookupTarget(staticFields, field);
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java
index f0f35bb..c3cd006 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java
@@ -4,9 +4,15 @@
 
 package com.android.tools.r8.ir.analysis.proto;
 
+import static com.android.tools.r8.ir.analysis.proto.ProtoUtils.getInfoValueFromMessageInfoConstructionInvoke;
+import static com.android.tools.r8.ir.analysis.proto.ProtoUtils.getObjectsValueFromMessageInfoConstructionInvoke;
+import static com.android.tools.r8.ir.analysis.proto.ProtoUtils.setObjectsValueForMessageInfoConstructionInvoke;
+import static com.google.common.base.Predicates.alwaysFalse;
+
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.ir.analysis.proto.schema.ProtoMessageInfo;
 import com.android.tools.r8.ir.analysis.proto.schema.ProtoObject;
 import com.android.tools.r8.ir.analysis.type.Nullability;
@@ -22,9 +28,13 @@
 import com.android.tools.r8.ir.code.MemberType;
 import com.android.tools.r8.ir.code.NewArrayEmpty;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.CallSiteInformation;
+import com.android.tools.r8.ir.conversion.IRConverter;
+import com.android.tools.r8.ir.conversion.OptimizationFeedbackIgnore;
+import com.android.tools.r8.ir.optimize.Outliner;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.BooleanUtils;
 import java.util.List;
+import java.util.function.Consumer;
 
 public class GeneratedMessageLiteShrinker {
 
@@ -55,11 +65,31 @@
   }
 
   public void run(DexEncodedMethod method, IRCode code) {
-    if (appView.options().isMinifying() && references.isDynamicMethod(method.method)) {
+    if (references.isDynamicMethod(method.method)) {
       rewriteDynamicMethod(method, code);
     }
   }
 
+  public void postOptimizeDynamicMethods(IRConverter converter) {
+    forEachDynamicMethod(
+        method ->
+            converter.processMethod(
+                method,
+                OptimizationFeedbackIgnore.getInstance(),
+                alwaysFalse(),
+                CallSiteInformation.empty(),
+                Outliner::noProcessing));
+  }
+
+  private void forEachDynamicMethod(Consumer<DexEncodedMethod> consumer) {
+    for (DexProgramClass clazz : appView.appInfo().classes()) {
+      DexEncodedMethod dynamicMethod = clazz.lookupVirtualMethod(references::isDynamicMethod);
+      if (dynamicMethod != null) {
+        consumer.accept(dynamicMethod);
+      }
+    }
+  }
+
   /**
    * Finds all const-string instructions in the code that flows into GeneratedMessageLite.
    * newMessageInfo(), and rewrites them into a dex-item-based-const-string if the string value
@@ -76,16 +106,10 @@
 
     InvokeMethod newMessageInfoInvoke = getNewMessageInfoInvoke(code, references);
     if (newMessageInfoInvoke != null) {
-      // If this invoke is targeting RawMessageInfo.<init>(...) then `info` and `objects` is at
-      // positions 2 and 3, respectively, and not position 1 and 2 as when calling the static method
-      // GeneratedMessageLite.newMessageInfo().
-      int adjustment = BooleanUtils.intValue(newMessageInfoInvoke.isInvokeDirect());
-      assert adjustment == 0
-          ? newMessageInfoInvoke.getInvokedMethod().match(references.newMessageInfoMethod)
-          : newMessageInfoInvoke.getInvokedMethod() == references.rawMessageInfoConstructor;
-
-      Value infoValue = newMessageInfoInvoke.inValues().get(1 + adjustment).getAliasedValue();
-      Value objectsValue = newMessageInfoInvoke.inValues().get(2 + adjustment).getAliasedValue();
+      Value infoValue =
+          getInfoValueFromMessageInfoConstructionInvoke(newMessageInfoInvoke, references);
+      Value objectsValue =
+          getObjectsValueFromMessageInfoConstructionInvoke(newMessageInfoInvoke, references);
 
       // Decode the arguments passed to newMessageInfo().
       ProtoMessageInfo protoMessageInfo = decoder.run(context, infoValue, objectsValue);
@@ -158,8 +182,8 @@
 
     // Pass the newly created `objects` array to RawMessageInfo.<init>(...) or
     // GeneratedMessageLite.newMessageInfo().
-    int adjustment = BooleanUtils.intValue(newMessageInfoInvoke.isInvokeDirect());
-    newMessageInfoInvoke.replaceValue(2 + adjustment, newObjectsValue);
+    setObjectsValueForMessageInfoConstructionInvoke(
+        newMessageInfoInvoke, newObjectsValue, references);
 
     if (hasIntroducedIdentifierNameString) {
       method.getMutableOptimizationInfo().markUseIdentifierNameString();
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/RawMessageInfoDecoder.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/RawMessageInfoDecoder.java
index f9b1d62..dd627f8 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/RawMessageInfoDecoder.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/RawMessageInfoDecoder.java
@@ -8,9 +8,11 @@
 import static com.android.tools.r8.ir.analysis.proto.ProtoUtils.getObjectsValueFromMessageInfoConstructionInvoke;
 
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.ir.analysis.proto.schema.DeadProtoFieldObject;
 import com.android.tools.r8.ir.analysis.proto.schema.ProtoFieldInfo;
 import com.android.tools.r8.ir.analysis.proto.schema.ProtoFieldObject;
 import com.android.tools.r8.ir.analysis.proto.schema.ProtoFieldType;
@@ -119,24 +121,18 @@
       ThrowingIterator<Value, InvalidRawMessageInfoException> objectIterator =
           createObjectIterator(objectsValue);
 
-      if (numberOfOneOfObjects > 0) {
-        builder.setNumberOfOneOfObjects(numberOfOneOfObjects);
-        for (int i = 0; i < numberOfOneOfObjects; i++) {
-          builder.addOneOfObject(
-              createProtoObject(
-                  objectIterator.computeNextIfAbsent(this::invalidObjectsFailure), context),
-              createProtoObject(
-                  objectIterator.computeNextIfAbsent(this::invalidObjectsFailure), context));
-        }
+      for (int i = 0; i < numberOfOneOfObjects; i++) {
+        builder.addOneOfObject(
+            createProtoObject(
+                objectIterator.computeNextIfAbsent(this::invalidObjectsFailure), context),
+            createProtoObject(
+                objectIterator.computeNextIfAbsent(this::invalidObjectsFailure), context));
       }
 
-      if (numberOfHasBitsObjects > 0) {
-        builder.setNumberOfHasBitsObjects(numberOfHasBitsObjects);
-        for (int i = 0; i < numberOfHasBitsObjects; i++) {
-          builder.addHasBitsObject(
-              createProtoObject(
-                  objectIterator.computeNextIfAbsent(this::invalidObjectsFailure), context));
-        }
+      for (int i = 0; i < numberOfHasBitsObjects; i++) {
+        builder.addHasBitsObject(
+            createProtoObject(
+                objectIterator.computeNextIfAbsent(this::invalidObjectsFailure), context));
       }
 
       boolean isProto2 = (flags & IS_PROTO_2_MASK) != 0;
@@ -193,6 +189,9 @@
         if (field != null) {
           return new ProtoFieldObject(field);
         }
+        // This const-string refers to a field that no longer exists. In this case, we create a
+        // special dead-object instead of failing with an InvalidRawMessageInfoException below.
+        return new DeadProtoFieldObject(context.type, constString.getValue());
       } else if (definition.isDexItemBasedConstString()) {
         DexItemBasedConstString constString = definition.asDexItemBasedConstString();
         DexReference reference = constString.getItem();
@@ -200,7 +199,14 @@
         if (reference.isDexField()
             && nameComputationInfo.isFieldNameComputationInfo()
             && nameComputationInfo.asFieldNameComputationInfo().isForFieldName()) {
-          return new ProtoFieldObject(reference.asDexField());
+          DexField field = reference.asDexField();
+          DexEncodedField encodedField = context.lookupInstanceField(field);
+          if (encodedField != null) {
+            return new ProtoFieldObject(field);
+          }
+          // This const-string refers to a field that no longer exists. In this case, we create a
+          // special dead-object instead of failing with an InvalidRawMessageInfoException below.
+          return new DeadProtoFieldObject(context.type, field.name);
         }
       } else if (definition.isInvokeStatic()) {
         InvokeStatic invoke = definition.asInvokeStatic();
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/DeadProtoFieldObject.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/DeadProtoFieldObject.java
new file mode 100644
index 0000000..3d2b3bc
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/DeadProtoFieldObject.java
@@ -0,0 +1,39 @@
+// 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.ir.analysis.proto.schema;
+
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+
+public class DeadProtoFieldObject extends ProtoObject {
+
+  // For debugging purposes only.
+  private final DexType holder;
+  private final DexString name;
+
+  public DeadProtoFieldObject(DexType holder, DexString name) {
+    this.holder = holder;
+    this.name = name;
+  }
+
+  @Override
+  public Instruction buildIR(AppView<?> appView, IRCode code) {
+    throw new Unreachable();
+  }
+
+  @Override
+  public boolean isDeadProtoFieldObject() {
+    return true;
+  }
+
+  @Override
+  public String toString() {
+    return "DeadProtoFieldObject(" + holder.toSourceString() + "." + name.toSourceString() + ")";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoEnqueuerExtension.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoEnqueuerExtension.java
index 1d77c62..15d5487 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoEnqueuerExtension.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoEnqueuerExtension.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.graph.analysis.EnqueuerAnalysis;
 import com.android.tools.r8.ir.analysis.proto.GeneratedMessageLiteShrinker;
 import com.android.tools.r8.ir.analysis.proto.ProtoReferences;
+import com.android.tools.r8.ir.analysis.proto.ProtoShrinker;
 import com.android.tools.r8.ir.analysis.proto.RawMessageInfoDecoder;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.InvokeMethod;
@@ -55,10 +56,11 @@
       new IdentityHashMap<>();
 
   public ProtoEnqueuerExtension(AppView<?> appView) {
+    ProtoShrinker protoShrinker = appView.protoShrinker();
     this.appView = appView;
-    this.decoder = appView.protoShrinker().decoder;
-    this.factory = appView.protoShrinker().factory;
-    this.references = appView.protoShrinker().references;
+    this.decoder = protoShrinker.decoder;
+    this.factory = protoShrinker.factory;
+    this.references = protoShrinker.references;
   }
 
   /**
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoFieldInfo.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoFieldInfo.java
index d8af9b6..d9ef276 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoFieldInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoFieldInfo.java
@@ -16,7 +16,17 @@
   private final int number;
   private final ProtoFieldType type;
 
+  /**
+   * Index into {@link ProtoMessageInfo#oneOfObjects} or {@link ProtoMessageInfo#hasBitsObjects}.
+   * Only used for oneof and proto2 singular fields.
+   */
   private final OptionalInt auxData;
+
+  /**
+   * For any non-oneof field, the first entry will be a reference to a java.lang.String literal. For
+   * repeated message fields, the second entry will be a reference to java.lang.Class for the
+   * message.
+   */
   private final List<ProtoObject> objects;
 
   public ProtoFieldInfo(
@@ -163,4 +173,23 @@
     assert object.isProtoFieldObject();
     return object.asProtoFieldObject().getField();
   }
+
+  @Override
+  public String toString() {
+    StringBuilder builder =
+        new StringBuilder("ProtoFieldInfo(number=")
+            .append(number)
+            .append(", type=")
+            .append(type)
+            .append(", aux data=")
+            .append(auxData)
+            .append(", objects=[");
+    if (objects.size() > 0) {
+      builder.append(objects.get(0));
+      for (int i = 1; i < objects.size(); i++) {
+        builder.append(", ").append(objects.get(i));
+      }
+    }
+    return builder.append("])").toString();
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoFieldObject.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoFieldObject.java
index dbd3c7f..b7e708b 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoFieldObject.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoFieldObject.java
@@ -50,4 +50,9 @@
   public ProtoFieldObject asProtoFieldObject() {
     return this;
   }
+
+  @Override
+  public String toString() {
+    return "ProtoFieldObject(" + field.toSourceString() + ")";
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoMessageInfo.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoMessageInfo.java
index d0871df..aaea56f 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoMessageInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoMessageInfo.java
@@ -7,8 +7,12 @@
 import static com.android.tools.r8.ir.analysis.proto.RawMessageInfoDecoder.IS_PROTO_2_MASK;
 
 import com.android.tools.r8.utils.Pair;
-import java.util.ArrayList;
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+import it.unimi.dsi.fastutil.ints.IntList;
+import java.util.Iterator;
+import java.util.LinkedList;
 import java.util.List;
+import java.util.function.Predicate;
 
 public class ProtoMessageInfo {
 
@@ -18,9 +22,9 @@
 
     private int flags;
 
-    private List<ProtoFieldInfo> fields;
-    private List<ProtoObject> hasBitsObjects;
-    private List<Pair<ProtoObject, ProtoObject>> oneOfObjects;
+    private LinkedList<ProtoFieldInfo> fields;
+    private LinkedList<ProtoObject> hasBitsObjects;
+    private LinkedList<Pair<ProtoObject, ProtoObject>> oneOfObjects;
 
     public void setFlags(int value) {
       this.flags = value;
@@ -28,53 +32,98 @@
 
     public void addField(ProtoFieldInfo field) {
       if (fields == null) {
-        fields = new ArrayList<>();
+        fields = new LinkedList<>();
       }
       fields.add(field);
     }
 
     public void addHasBitsObject(ProtoObject hasBitsObject) {
       if (hasBitsObjects == null) {
-        hasBitsObjects = new ArrayList<>();
+        hasBitsObjects = new LinkedList<>();
       }
       hasBitsObjects.add(hasBitsObject);
     }
 
-    public void setNumberOfHasBitsObjects(int number) {
-      if (number > 0) {
-        hasBitsObjects = new ArrayList<>(number);
-      }
-    }
-
     public void addOneOfObject(ProtoObject first, ProtoObject second) {
       if (oneOfObjects == null) {
-        oneOfObjects = new ArrayList<>();
+        oneOfObjects = new LinkedList<>();
       }
       oneOfObjects.add(new Pair<>(first, second));
     }
 
-    public void setNumberOfOneOfObjects(int number) {
-      if (number > 0) {
-        oneOfObjects = new ArrayList<>(number);
+    public ProtoMessageInfo build() {
+      removeDeadFields();
+      removeUnusedSharedData();
+      return new ProtoMessageInfo(flags, fields, hasBitsObjects, oneOfObjects);
+    }
+
+    private void removeDeadFields() {
+      if (fields != null) {
+        Predicate<ProtoFieldInfo> isFieldDead =
+            field -> {
+              ProtoObject object =
+                  field.getType().isOneOf()
+                      ? oneOfObjects.get(field.getAuxData()).getFirst()
+                      : field.getObjects().get(0);
+              return object.isDeadProtoFieldObject();
+            };
+        fields.removeIf(isFieldDead);
       }
     }
 
-    public ProtoMessageInfo build() {
-      return new ProtoMessageInfo(flags, fields, hasBitsObjects, oneOfObjects);
+    private void removeUnusedSharedData() {
+      // Gather used "oneof" and "hasbits" indices.
+      IntList usedOneofIndices = new IntArrayList();
+      IntList usedHasBitsIndices = new IntArrayList();
+      if (fields != null) {
+        for (ProtoFieldInfo field : fields) {
+          if (field.hasAuxData()) {
+            if (field.getType().isOneOf()) {
+              usedOneofIndices.add(field.getAuxData());
+            } else {
+              usedHasBitsIndices.add(field.getAuxData() / BITS_PER_HAS_BITS_WORD);
+            }
+          }
+        }
+      }
+
+      // Remove unused parts of "oneof" vector.
+      if (oneOfObjects != null) {
+        Iterator<Pair<ProtoObject, ProtoObject>> oneOfObjectIterator = oneOfObjects.iterator();
+        for (int i = 0; i < oneOfObjects.size(); i++) {
+          oneOfObjectIterator.next();
+          if (!usedOneofIndices.contains(i)) {
+            oneOfObjectIterator.remove();
+          }
+        }
+      }
+
+      // Remove unused parts of "hasbits" vector.
+      if (hasBitsObjects != null) {
+        Iterator<ProtoObject> hasBitsObjectIterator = hasBitsObjects.iterator();
+        for (int i = 0; i < hasBitsObjects.size(); i++) {
+          hasBitsObjectIterator.next();
+          if (!usedHasBitsIndices.contains(i)) {
+            hasBitsObjectIterator.remove();
+          }
+        }
+      }
+
+      // TODO(b/112437944): Fix up references + add a test that fails when references are not fixed.
     }
   }
 
   private final int flags;
 
-  private final List<ProtoFieldInfo> fields;
-  private final List<ProtoObject> hasBitsObjects;
-  private final List<Pair<ProtoObject, ProtoObject>> oneOfObjects;
+  private final LinkedList<ProtoFieldInfo> fields;
+  private final LinkedList<ProtoObject> hasBitsObjects;
+  private final LinkedList<Pair<ProtoObject, ProtoObject>> oneOfObjects;
 
   private ProtoMessageInfo(
       int flags,
-      List<ProtoFieldInfo> fields,
-      List<ProtoObject> hasBitsObjects,
-      List<Pair<ProtoObject, ProtoObject>> oneOfObjects) {
+      LinkedList<ProtoFieldInfo> fields,
+      LinkedList<ProtoObject> hasBitsObjects,
+      LinkedList<Pair<ProtoObject, ProtoObject>> oneOfObjects) {
     this.flags = flags;
     this.fields = fields;
     this.hasBitsObjects = hasBitsObjects;
@@ -120,4 +169,17 @@
   public int numberOfOneOfObjects() {
     return oneOfObjects != null ? oneOfObjects.size() : 0;
   }
+
+  @Override
+  public String toString() {
+    StringBuilder builder = new StringBuilder("ProtoMessageInfo(fields=[");
+    if (hasFields()) {
+      Iterator<ProtoFieldInfo> fieldIterator = fields.iterator();
+      builder.append(fieldIterator.next());
+      while (fieldIterator.hasNext()) {
+        builder.append(", ").append(fieldIterator.next());
+      }
+    }
+    return builder.append("])").toString();
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoObject.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoObject.java
index 31ef09f..ca06081 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoObject.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoObject.java
@@ -12,6 +12,10 @@
 
   public abstract Instruction buildIR(AppView<?> appView, IRCode code);
 
+  public boolean isDeadProtoFieldObject() {
+    return false;
+  }
+
   public boolean isProtoFieldObject() {
     return false;
   }
diff --git a/src/test/java/com/android/tools/r8/internal/proto/Proto2ShrinkingTest.java b/src/test/java/com/android/tools/r8/internal/proto/Proto2ShrinkingTest.java
index 6a79796..c1b3f86 100644
--- a/src/test/java/com/android/tools/r8/internal/proto/Proto2ShrinkingTest.java
+++ b/src/test/java/com/android/tools/r8/internal/proto/Proto2ShrinkingTest.java
@@ -39,11 +39,6 @@
         .addProgramFiles(PROTO2_EXAMPLES_JAR, PROTO2_PROTO_JAR, PROTOBUF_LITE_JAR)
         .addKeepMainRule("proto2.TestClass")
         .addKeepRules(
-            // TODO(b/112437944): Do not remove proto fields that are actually used in tree shaking.
-            "-keepclassmembers,allowobfuscation class * extends",
-            "    com.google.protobuf.GeneratedMessageLite {",
-            "  <fields>;",
-            "}",
             allowAccessModification ? "-allowaccessmodification" : "")
         .addKeepRuleFiles(PROTOBUF_LITE_PROGUARD_RULES)
         .addOptionsModification(
diff --git a/src/test/java/com/android/tools/r8/internal/proto/Proto3ShrinkingTest.java b/src/test/java/com/android/tools/r8/internal/proto/Proto3ShrinkingTest.java
index 38bdf2b..7e876c9 100644
--- a/src/test/java/com/android/tools/r8/internal/proto/Proto3ShrinkingTest.java
+++ b/src/test/java/com/android/tools/r8/internal/proto/Proto3ShrinkingTest.java
@@ -39,11 +39,6 @@
         .addProgramFiles(PROTO3_EXAMPLES_JAR, PROTO3_PROTO_JAR, PROTOBUF_LITE_JAR)
         .addKeepMainRule("proto3.TestClass")
         .addKeepRules(
-            // TODO(b/112437944): Do not remove proto fields that are actually used in tree shaking.
-            "-keepclassmembers,allowobfuscation class * extends",
-            "    com.google.protobuf.GeneratedMessageLite {",
-            "  <fields>;",
-            "}",
             allowAccessModification ? "-allowaccessmodification" : "")
         .addKeepRuleFiles(PROTOBUF_LITE_PROGUARD_RULES)
         .addOptionsModification(