Add additional string builder tests

Bug: b/190489514
Bug: b/219455761
Bug: b/113859361
Bug: b/222437581
Change-Id: I091a0fa60e7bf80698c861e896ff36ab593013ee
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderTests.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderTests.java
new file mode 100644
index 0000000..f908b95
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderTests.java
@@ -0,0 +1,525 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.string;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeMatchers;
+import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.function.Function;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class StringBuilderTests extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameter(1)
+  public StringBuilderResult stringBuilderTest;
+
+  @Parameters(name = "{0}, configuration: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimesAndApiLevels().build(), getTestExpectations());
+  }
+
+  private static class StringBuilderResult {
+
+    private final Method method;
+    private final String expected;
+    private final int stringBuilders;
+    private final int appends;
+    private final int toStrings;
+
+    private StringBuilderResult(
+        Method method, String expected, int stringBuilders, int appends, int toStrings) {
+      this.method = method;
+      this.expected = expected;
+      this.stringBuilders = stringBuilders;
+      this.appends = appends;
+      this.toStrings = toStrings;
+    }
+
+    private static StringBuilderResult create(
+        Method method, String expected, int stringBuilders, int appends, int toStrings) {
+      return new StringBuilderResult(method, expected, stringBuilders, appends, toStrings);
+    }
+
+    @Override
+    public String toString() {
+      return getMethodName();
+    }
+
+    String getMethodName() {
+      return method.getName();
+    }
+  }
+
+  private static StringBuilderResult[] getTestExpectations() {
+    try {
+      return new StringBuilderResult[] {
+        StringBuilderResult.create(
+            Main.class.getMethod("emptyStringTest"), StringUtils.lines(""), 0, 0, 0),
+        StringBuilderResult.create(
+            Main.class.getMethod("simpleStraightLineTest"),
+            StringUtils.lines("Hello World"),
+            0,
+            0,
+            0),
+        StringBuilderResult.create(
+            Main.class.getMethod("notMaterializing"), StringUtils.lines("Hello World"), 0, 0, 0),
+        StringBuilderResult.create(
+            Main.class.getMethod("materializingWithAdditionalUnObservedAppend"),
+            StringUtils.lines("Hello World"),
+            0,
+            0,
+            0),
+        StringBuilderResult.create(
+            Main.class.getMethod("materializingWithAdditionalAppend"),
+            StringUtils.lines("Hello World", "Hello WorldObservable"),
+            1,
+            3,
+            2),
+        StringBuilderResult.create(
+            Main.class.getMethod("appendWithNonConstant"),
+            StringUtils.lines("Hello World, Hello World"),
+            1,
+            3,
+            1),
+        StringBuilderResult.create(
+            Main.class.getMethod("simpleLoopTest"),
+            StringUtils.lines("Hello WorldHello World"),
+            1,
+            1,
+            1),
+        // TODO(b/222437581): Should not remove StringBuilder
+        StringBuilderResult.create(
+            Main.class.getMethod("simpleLoopTest2"),
+            StringUtils.lines("Hello World", "Hello WorldHello World"),
+            0,
+            0,
+            0),
+        StringBuilderResult.create(
+            Main.class.getMethod("simpleLoopWithStringBuilderInBodyTest"),
+            StringUtils.lines("Hello World"),
+            0,
+            0,
+            0),
+        StringBuilderResult.create(
+            Main.class.getMethod("simpleDiamondTest"),
+            StringUtils.lines("Message: Hello World"),
+            0,
+            0,
+            0),
+        StringBuilderResult.create(
+            Main.class.getMethod("diamondWithUseTest"), StringUtils.lines("Hello World"), 1, 3, 1),
+        StringBuilderResult.create(
+            Main.class.getMethod("diamondsWithSingleUseTest"),
+            StringUtils.lines("Hello World"),
+            1,
+            3,
+            1),
+        StringBuilderResult.create(
+            Main.class.getMethod("escapeTest"), StringUtils.lines("Hello World"), 2, 2, 1),
+        StringBuilderResult.create(
+            Main.class.getMethod("intoPhiTest"), StringUtils.lines("Hello World"), 2, 2, 1),
+        StringBuilderResult.create(
+            Main.class.getMethod("optimizePartial"), StringUtils.lines("Hello World.."), 1, 4, 1),
+        StringBuilderResult.create(
+            Main.class.getMethod("multipleToStrings"),
+            StringUtils.lines("Hello World", "Hello World.."),
+            1,
+            4,
+            2),
+        StringBuilderResult.create(
+            Main.class.getMethod("changeAppendType"), StringUtils.lines("1 World"), 1, 3, 1),
+        StringBuilderResult.create(
+            Main.class.getMethod("checkCapacity"), StringUtils.lines("true"), 2, 1, 0),
+        StringBuilderResult.create(
+            Main.class.getMethod("checkHashCode"), StringUtils.lines("false"), 1, 0, 0),
+        StringBuilderResult.create(
+            Main.class.getMethod("stringBuilderWithStringBuilderToString"),
+            StringUtils.lines("Hello World"),
+            1,
+            1,
+            1),
+        StringBuilderResult.create(
+            Main.class.getMethod("stringBuilderWithStringBuilder"),
+            StringUtils.lines("Hello World"),
+            2,
+            2,
+            1),
+        StringBuilderResult.create(
+            Main.class.getMethod("stringBuilderInStringBuilderConstructor"),
+            StringUtils.lines("Hello World"),
+            2,
+            1,
+            1),
+        StringBuilderResult.create(
+            Main.class.getMethod("interDependencyTest"),
+            StringUtils.lines("World Hello World "),
+            2,
+            2,
+            1),
+        StringBuilderResult.create(
+            Main.class.getMethod("stringBuilderSelfReference"), StringUtils.lines(""), 1, 1, 1),
+        StringBuilderResult.create(
+            Main.class.getMethod("unknownStringBuilderInstruction"),
+            StringUtils.lines("Hello World"),
+            1,
+            2,
+            1),
+      };
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private static final Function<TestParameters, R8TestCompileResult> compilationResults =
+      memoizeFunction(StringBuilderTests::compileR8);
+
+  private static R8TestCompileResult compileR8(TestParameters parameters) throws Exception {
+    return testForR8(getStaticTemp(), parameters.getBackend())
+        .addProgramClasses(Main.class)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepClassAndMembersRules(Main.class)
+        .enableInliningAnnotations()
+        .compile();
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(Main.class)
+        .run(parameters.getRuntime(), Main.class, stringBuilderTest.getMethodName())
+        .assertSuccessWithOutput(stringBuilderTest.expected);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    boolean hasError =
+        stringBuilderTest.getMethodName().equals("simpleLoopTest2") && parameters.isDexRuntime();
+    compilationResults
+        .apply(parameters)
+        .inspect(
+            inspector -> {
+              if (parameters.isCfRuntime()) {
+                // TODO(b/114002137): for now, string concatenation depends on rewriteMoveResult.
+                return;
+              }
+              MethodSubject method = inspector.method(stringBuilderTest.method);
+              assertThat(method, isPresent());
+              FoundMethodSubject foundMethodSubject = method.asFoundMethodSubject();
+              assertEquals(
+                  stringBuilderTest.stringBuilders, countStringBuilderInits(foundMethodSubject));
+              assertEquals(
+                  stringBuilderTest.appends, countStringBuilderAppends(foundMethodSubject));
+              assertEquals(
+                  stringBuilderTest.toStrings, countStringBuilderToStrings(foundMethodSubject));
+            })
+        .run(parameters.getRuntime(), Main.class, stringBuilderTest.getMethodName())
+        // TODO(b/222437581): Incorrect result for string builder inside loop.
+        .assertSuccessWithOutputLinesIf(hasError, "Hello World", "Hello World")
+        .assertSuccessWithOutputIf(!hasError, stringBuilderTest.expected);
+  }
+
+  private long countStringBuilderInits(FoundMethodSubject method) {
+    return countInstructionsOnStringBuilder(method, "<init>");
+  }
+
+  private long countStringBuilderAppends(FoundMethodSubject method) {
+    return countInstructionsOnStringBuilder(method, "append");
+  }
+
+  private long countStringBuilderToStrings(FoundMethodSubject method) {
+    return countInstructionsOnStringBuilder(method, "toString");
+  }
+
+  private long countInstructionsOnStringBuilder(FoundMethodSubject method, String methodName) {
+    return method
+        .streamInstructions()
+        .filter(
+            instructionSubject ->
+                CodeMatchers.isInvokeWithTarget(typeName(StringBuilder.class), methodName)
+                    .test(instructionSubject))
+        .count();
+  }
+
+  public static class Main {
+
+    @NeverInline
+    public static void emptyStringTest() {
+      StringBuilder sb = new StringBuilder();
+      System.out.println(sb.toString());
+    }
+
+    @NeverInline
+    public static void simpleStraightLineTest() {
+      StringBuilder sb = new StringBuilder();
+      sb = sb.append("Hello ");
+      sb.append("World");
+      System.out.println(sb.toString());
+    }
+
+    @NeverInline
+    public static void notMaterializing() {
+      StringBuilder sb = new StringBuilder();
+      sb.append("foo");
+      if (System.currentTimeMillis() > 0) {
+        sb.append("bar");
+      }
+      System.out.println("Hello World");
+    }
+
+    @NeverInline
+    public static void materializingWithAdditionalUnObservedAppend() {
+      StringBuilder sb = new StringBuilder();
+      sb.append("Hello ");
+      sb.append("World");
+      System.out.println(sb.toString());
+      sb.append("Not observable");
+    }
+
+    @NeverInline
+    public static void materializingWithAdditionalAppend() {
+      StringBuilder sb = new StringBuilder();
+      sb.append("Hello ");
+      sb.append("World");
+      System.out.println(sb.toString());
+      sb.append("Observable");
+      System.out.println(sb.toString());
+    }
+
+    @NeverInline
+    public static void appendWithNonConstant() {
+      StringBuilder sb = new StringBuilder();
+      sb.append("Hello ");
+      String other;
+      if (System.currentTimeMillis() > 0) {
+        other = "World, Hello ";
+      } else {
+        other = "Hello World";
+      }
+      sb.append(other);
+      sb.append("World");
+      System.out.println(sb.toString());
+    }
+
+    @NeverInline
+    public static void simpleLoopTest() {
+      int count = System.currentTimeMillis() > 0 ? 2 : 0;
+      StringBuilder sb = new StringBuilder();
+      for (int i = 0; i < count; i++) {
+        sb.append("Hello World");
+      }
+      System.out.println(sb.toString());
+    }
+
+    @NeverInline
+    public static void simpleLoopTest2() {
+      int count = System.currentTimeMillis() > 0 ? 2 : 0;
+      StringBuilder sb = new StringBuilder();
+      for (int i = 0; i < count; i++) {
+        sb.append("Hello World");
+        System.out.println(sb.toString());
+      }
+    }
+
+    @NeverInline
+    public static void simpleLoopWithStringBuilderInBodyTest() {
+      int count = System.currentTimeMillis() > 0 ? 1 : 0;
+      while (count > 0) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("Hello ");
+        sb.append("World");
+        System.out.println(sb.toString());
+        count--;
+      }
+    }
+
+    @NeverInline
+    public static void simpleDiamondTest() {
+      StringBuilder sb = new StringBuilder();
+      sb.append("Hello ");
+      if (System.currentTimeMillis() > 0) {
+        System.out.print("Message: ");
+      } else {
+        throw new RuntimeException();
+      }
+      sb.append("World");
+      System.out.println(sb.toString());
+    }
+
+    @NeverInline
+    public static void diamondWithUseTest() {
+      StringBuilder sb = new StringBuilder();
+      sb.append("Hello");
+      if (System.currentTimeMillis() > 0) {
+        sb.append(" ");
+      } else {
+        sb.append("Planet");
+      }
+      sb.append("World");
+      System.out.println(sb.toString());
+    }
+
+    @NeverInline
+    public static void diamondsWithSingleUseTest() {
+      StringBuilder sb = new StringBuilder();
+      sb.append("Hello");
+      if (System.currentTimeMillis() > 0) {
+        sb.append(" ");
+      }
+      sb.append("World");
+      System.out.println(sb.toString());
+    }
+
+    @NeverInline
+    public static void escapeTest() {
+      StringBuilder sb = new StringBuilder();
+      sb.append("Hello");
+      StringBuilder sbObject;
+      if (System.currentTimeMillis() > 0) {
+        sbObject = sb;
+      } else {
+        sbObject = new StringBuilder();
+      }
+      escape(sbObject);
+      sb.append("World");
+      System.out.println(sb.toString());
+    }
+
+    @NeverInline
+    public static void escape(Object obj) {
+      ((StringBuilder) obj).append(" ");
+    }
+
+    @NeverInline
+    public static void intoPhiTest() {
+      StringBuilder sb;
+      if (System.currentTimeMillis() > 0) {
+        sb = new StringBuilder();
+        sb.append("Hello ");
+      } else {
+        sb = new StringBuilder();
+        sb.append("Other ");
+      }
+      sb.append("World");
+      System.out.println(sb.toString());
+    }
+
+    @NeverInline
+    public static void optimizePartial() {
+      StringBuilder sb = new StringBuilder();
+      sb.append("Hello ");
+      if (System.currentTimeMillis() > 0) {
+        sb.append("World");
+      }
+      sb.append(".");
+      sb.append(".");
+      System.out.println(sb.toString());
+    }
+
+    @NeverInline
+    public static void multipleToStrings() {
+      StringBuilder sb = new StringBuilder();
+      sb.append("Hello ");
+      sb.append("World");
+      System.out.println(sb.toString());
+      sb.append(".");
+      sb.append(".");
+      System.out.println(sb.toString());
+    }
+
+    @NeverInline
+    public static void changeAppendType() {
+      StringBuilder sb = new StringBuilder();
+      if (System.currentTimeMillis() == 0) {
+        sb.append("foo");
+      }
+      sb.append(1);
+      sb.append(" World");
+      System.out.println(sb.toString());
+    }
+
+    @NeverInline
+    public static void checkCapacity() {
+      StringBuilder stringBuilder = new StringBuilder();
+      stringBuilder.append("foo");
+      StringBuilder otherBuilder = new StringBuilder("foo");
+      System.out.println(stringBuilder.capacity() != otherBuilder.capacity());
+    }
+
+    @NeverInline
+    public static void checkHashCode() {
+      StringBuilder sb = new StringBuilder();
+      System.out.println(sb.hashCode() == 0);
+    }
+
+    @NeverInline
+    public static void stringBuilderWithStringBuilderToString() {
+      System.out.println(
+          new StringBuilder()
+              .append(new StringBuilder().append("Hello World").toString())
+              .toString());
+    }
+
+    @NeverInline
+    public static void stringBuilderWithStringBuilder() {
+      System.out.println(
+          new StringBuilder().append(new StringBuilder().append("Hello World")).toString());
+    }
+
+    @NeverInline
+    public static void stringBuilderInStringBuilderConstructor() {
+      System.out.println(new StringBuilder(new StringBuilder().append("Hello World")).toString());
+    }
+
+    @NeverInline
+    public static void interDependencyTest() {
+      StringBuilder sb1 = new StringBuilder("Hello ");
+      StringBuilder sb2 = new StringBuilder("World ");
+      sb1.append(sb2);
+      sb2.append(sb1);
+      System.out.println(sb2.toString());
+    }
+
+    @NeverInline
+    public static void stringBuilderSelfReference() {
+      StringBuilder sb = new StringBuilder();
+      sb.append(sb);
+      System.out.println(sb.toString());
+    }
+
+    @NeverInline
+    public static void unknownStringBuilderInstruction() {
+      StringBuilder sb = new StringBuilder();
+      sb.append("Helloo ");
+      sb.deleteCharAt(5);
+      sb.append("World");
+      System.out.println(sb.toString());
+    }
+
+    public static void main(String[] args) throws Exception {
+      Method method = Main.class.getMethod(args[0]);
+      method.invoke(null);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
index 76427a3..50a77fe 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
@@ -194,6 +194,11 @@
   }
 
   public static Predicate<InstructionSubject> isInvokeWithTarget(
+      String holderType, String methodName) {
+    return isInvokeWithTarget(null, holderType, methodName, (List<String>) null);
+  }
+
+  public static Predicate<InstructionSubject> isInvokeWithTarget(
       String returnType, String holderType, String methodName, List<String> parameterTypes) {
     return instruction -> {
       if (!instruction.isInvoke()) {