Extend unused StringBuilder removal to Objects.toString() and String.valueOf()

Bug: 174285670
Change-Id: Ia7d6df07abd04fdfaa5e23f44d19574dbed36280
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectsMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectsMethodOptimizer.java
index 9d0c8c8..5fc8d62 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectsMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectsMethodOptimizer.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.utils.ValueUtils;
 import java.util.Set;
 
 public class ObjectsMethodOptimizer extends StatelessLibraryMethodModelCollection {
@@ -66,12 +67,21 @@
       InstructionListIterator instructionIterator,
       InvokeMethod invoke,
       Set<Value> affectedValues) {
+    // Optimize Objects.toString(null) into "null".
     Value object = invoke.getFirstArgument();
     if (object.getType().isDefinitelyNull()) {
       instructionIterator.replaceCurrentInstructionWithConstString(appView, code, "null");
       if (invoke.hasOutValue()) {
         affectedValues.addAll(invoke.outValue().affectedValues());
       }
+      return;
+    }
+
+    // Remove Objects.toString(stringBuilder) if unused.
+    if (ValueUtils.isStringBuilder(invoke.getFirstArgument(), dexItemFactory)) {
+      if (!invoke.hasOutValue() || !invoke.outValue().hasNonDebugUsers()) {
+        instructionIterator.removeOrReplaceByDebugLocalRead();
+      }
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/StringBuilderMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/StringBuilderMethodOptimizer.java
index 9cccb5d..99b1bda 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/StringBuilderMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/StringBuilderMethodOptimizer.java
@@ -4,7 +4,10 @@
 
 package com.android.tools.r8.ir.optimize.library;
 
+import static com.android.tools.r8.ir.code.Opcodes.ASSUME;
+import static com.android.tools.r8.ir.code.Opcodes.IF;
 import static com.android.tools.r8.ir.code.Opcodes.INVOKE_DIRECT;
+import static com.android.tools.r8.ir.code.Opcodes.INVOKE_STATIC;
 import static com.android.tools.r8.ir.code.Opcodes.INVOKE_VIRTUAL;
 import static com.android.tools.r8.ir.code.Opcodes.NEW_INSTANCE;
 
@@ -29,6 +32,7 @@
 import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations.UtilityMethodForCodeOptimizations;
 import com.android.tools.r8.ir.optimize.library.StringBuilderMethodOptimizer.State;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ValueUtils;
 import com.android.tools.r8.utils.WorkList;
 import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.objects.Reference2BooleanMap;
@@ -73,6 +77,8 @@
       InvokeMethodWithReceiver invokeWithReceiver = invoke.asInvokeMethodWithReceiver();
       if (stringBuilderMethods.isAppendMethod(singleTarget.getReference())) {
         optimizeAppend(code, instructionIterator, invokeWithReceiver, singleTarget, state);
+      } else if (singleTarget.getReference() == dexItemFactory.stringBuilderMethods.toString) {
+        optimizeToString(instructionIterator, invokeWithReceiver);
       }
     }
   }
@@ -130,6 +136,16 @@
     }
   }
 
+  private void optimizeToString(
+      InstructionListIterator instructionIterator, InvokeMethodWithReceiver invoke) {
+    // Optimize StringBuilder.toString() if unused.
+    if (ValueUtils.isNonNullStringBuilder(invoke.getReceiver(), dexItemFactory)) {
+      if (!invoke.hasOutValue() || !invoke.outValue().hasNonDebugUsers()) {
+        instructionIterator.removeOrReplaceByDebugLocalRead();
+      }
+    }
+  }
+
   class State implements LibraryMethodModelCollection.State {
 
     final MethodProcessor methodProcessor;
@@ -185,6 +201,10 @@
 
         Instruction definition = alias.definition;
         switch (definition.opcode()) {
+          case ASSUME:
+            worklist.addIfNotSeen(definition.inValues());
+            break;
+
           case NEW_INSTANCE:
             assert definition.asNewInstance().clazz == dexItemFactory.stringBuilderType;
             break;
@@ -208,6 +228,14 @@
         // Analyze all users.
         for (Instruction user : alias.uniqueUsers()) {
           switch (user.opcode()) {
+            case ASSUME:
+              worklist.addIfNotSeen(user.outValue());
+              break;
+
+            case IF:
+              // StringBuilder null check.
+              break;
+
             case INVOKE_DIRECT:
               {
                 InvokeDirect invoke = user.asInvokeDirect();
@@ -224,6 +252,25 @@
               }
               break;
 
+            case INVOKE_STATIC:
+              {
+                InvokeStatic invoke = user.asInvokeStatic();
+                DexMethod invokedMethod = invoke.getInvokedMethod();
+
+                // Allow calls to Objects.toString(Object) and String.valueOf(Object).
+                if (invokedMethod == dexItemFactory.objectsMethods.toStringWithObject
+                    || invokedMethod == dexItemFactory.stringMembers.valueOf) {
+                  // Only allow unused StringBuilders.
+                  if (invoke.hasOutValue() && invoke.outValue().hasNonDebugUsers()) {
+                    return false;
+                  }
+                  break;
+                }
+
+                // Invoke to unhandled method, give up.
+                return false;
+              }
+
             case INVOKE_VIRTUAL:
               {
                 InvokeVirtual invoke = user.asInvokeVirtual();
@@ -245,7 +292,8 @@
                 }
 
                 // Allow calls to toString().
-                if (invokedMethod == stringBuilderMethods.toString) {
+                if (invokedMethod == dexItemFactory.objectMembers.toString
+                    || invokedMethod == stringBuilderMethods.toString) {
                   // Only allow unused StringBuilders.
                   if (invoke.hasOutValue() && invoke.outValue().hasNonDebugUsers()) {
                     return false;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/StringMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/StringMethodOptimizer.java
index c8d04ae..a8fabe3 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/StringMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/StringMethodOptimizer.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
@@ -15,7 +16,10 @@
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.InvokeStatic;
+import com.android.tools.r8.ir.code.InvokeVirtual;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.utils.ValueUtils;
 import java.util.Set;
 
 public class StringMethodOptimizer extends StatelessLibraryMethodModelCollection {
@@ -40,17 +44,20 @@
       InvokeMethod invoke,
       DexClassAndMethod singleTarget,
       Set<Value> affectedValues) {
-    if (singleTarget.getReference() == dexItemFactory.stringMembers.equals) {
-      optimizeEquals(code, instructionIterator, invoke);
+    DexMethod singleTargetReference = singleTarget.getReference();
+    if (singleTargetReference == dexItemFactory.stringMembers.equals) {
+      optimizeEquals(code, instructionIterator, invoke.asInvokeVirtual());
+    } else if (singleTargetReference == dexItemFactory.stringMembers.valueOf) {
+      optimizeValueOf(instructionIterator, invoke.asInvokeStatic());
     }
   }
 
   private void optimizeEquals(
-      IRCode code, InstructionListIterator instructionIterator, InvokeMethod invoke) {
+      IRCode code, InstructionListIterator instructionIterator, InvokeVirtual invoke) {
     if (appView.appInfo().hasLiveness()) {
       ProgramMethod context = code.context();
-      Value first = invoke.arguments().get(0).getAliasedValue();
-      Value second = invoke.arguments().get(1).getAliasedValue();
+      Value first = invoke.getReceiver().getAliasedValue();
+      Value second = invoke.getArgument(1).getAliasedValue();
       if (isPrunedClassNameComparison(first, second, context)
           || isPrunedClassNameComparison(second, first, context)) {
         instructionIterator.replaceCurrentInstructionWithConstInt(code, 0);
@@ -58,6 +65,15 @@
     }
   }
 
+  private void optimizeValueOf(InstructionListIterator instructionIterator, InvokeStatic invoke) {
+    // Optimize String.valueOf(stringBuilder) if unused.
+    if (ValueUtils.isNonNullStringBuilder(invoke.getFirstArgument(), dexItemFactory)) {
+      if (!invoke.hasOutValue() || !invoke.outValue().hasNonDebugUsers()) {
+        instructionIterator.removeOrReplaceByDebugLocalRead();
+      }
+    }
+  }
+
   /**
    * Returns true if {@param classNameValue} is defined by calling {@link Class#getName()} and
    * {@param constStringValue} is a constant string that is identical to the name of a class that
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizer.java
index a61662c..4dc7926 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizer.java
@@ -852,7 +852,8 @@
 
     @Override
     public boolean isToStringMethod(DexMethod method) {
-      return method == factory.stringBuilderMethods.toString
+      return method == factory.objectMembers.toString
+          || method == factory.stringBuilderMethods.toString
           || method == factory.stringBufferMethods.toString
           || method == factory.stringMembers.valueOf;
     }
diff --git a/src/main/java/com/android/tools/r8/utils/ValueUtils.java b/src/main/java/com/android/tools/r8/utils/ValueUtils.java
new file mode 100644
index 0000000..8121493
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/ValueUtils.java
@@ -0,0 +1,46 @@
+// Copyright (c) 2021, 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 com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeVirtual;
+import com.android.tools.r8.ir.code.NewInstance;
+import com.android.tools.r8.ir.code.Value;
+
+public class ValueUtils {
+
+  public static boolean isStringBuilder(Value value, DexItemFactory dexItemFactory) {
+    TypeElement type = value.getType();
+    return type.isClassType()
+        && type.asClassType().getClassType() == dexItemFactory.stringBuilderType;
+  }
+
+  public static boolean isNonNullStringBuilder(Value value, DexItemFactory dexItemFactory) {
+    while (true) {
+      if (value.isPhi()) {
+        return false;
+      }
+
+      Instruction definition = value.getDefinition();
+      if (definition.isNewInstance()) {
+        NewInstance newInstance = definition.asNewInstance();
+        return newInstance.clazz == dexItemFactory.stringBuilderType;
+      }
+
+      if (definition.isInvokeVirtual()) {
+        InvokeVirtual invoke = definition.asInvokeVirtual();
+        if (dexItemFactory.stringBuilderMethods.isAppendMethod(invoke.getInvokedMethod())) {
+          value = invoke.getReceiver();
+          continue;
+        }
+      }
+
+      // Unhandled definition.
+      return false;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderWithAppendObjectTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderWithAppendObjectTest.java
index d8ed11c..b8c0799 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderWithAppendObjectTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderWithAppendObjectTest.java
@@ -5,13 +5,14 @@
 package com.android.tools.r8.ir.optimize.string;
 
 import static com.android.tools.r8.utils.codeinspector.CodeMatchers.instantiatesClass;
-import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
+import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.util.Objects;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -40,11 +41,7 @@
         .inspect(
             inspector -> {
               MethodSubject mainMethod = inspector.clazz(Main.class).mainMethod();
-              assertThat(
-                  mainMethod,
-                  notIf(
-                      instantiatesClass(StringBuilder.class),
-                      canUseJavaUtilObjects(parameters) || parameters.isDexRuntime()));
+              assertThat(mainMethod, not(instantiatesClass(StringBuilder.class)));
             })
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithEmptyOutput();
@@ -55,6 +52,8 @@
     public static void main(String[] args) {
       A a = System.currentTimeMillis() > 0 ? new A() : null;
       new StringBuilder().append(a).toString();
+      Objects.toString(new StringBuilder().append(a));
+      String.valueOf(new StringBuilder().append(a));
     }
   }