Rewrite StringBuilder.appends with non-constants to String.concat

String.concat requires both the initial string and the argument string to be non-null.

Bug: b/129200243
Change-Id: I64c0db35fe7e243b6b5a4b84cc4322f9fec0cb11
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 8fe1c7e..44506fd 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -2118,7 +2118,7 @@
     }
 
     public boolean isAppendCharSequenceMethod(DexMethod method) {
-      return method == appendCharSequence;
+      return method == appendCharSequence || method == appendSubCharSequence;
     }
 
     public boolean isAppendObjectOrCharSequenceMethod(DexMethod method) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAction.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAction.java
index 05a91e5..4383519 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAction.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAction.java
@@ -7,6 +7,8 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.ir.analysis.type.Nullability;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionListIterator;
@@ -102,28 +104,7 @@
       Instruction previous = iterator.previous();
       InvokeMethodWithReceiver invoke = previous.asInvokeMethodWithReceiver();
       assert invoke != null;
-      // If the block has catch handlers, inserting a constant string in the same block as the
-      // append violates our block representation in DEX since constant string is throwing. If the
-      // append is in a block with catch handlers, we simply insert a new constant string in the
-      // entry block after all arguments.
-      Value value;
-      if (!invoke.getBlock().hasCatchHandlers()) {
-        value =
-            iterator.insertConstStringInstruction(
-                appView, code, appView.dexItemFactory().createString(replacement));
-      } else {
-        InstructionListIterator stringInsertIterator = code.entryBlock().listIterator(code);
-        while (stringInsertIterator.hasNext()) {
-          Instruction next = stringInsertIterator.next();
-          if (!next.isArgument()) {
-            stringInsertIterator.previous();
-            break;
-          }
-        }
-        value =
-            stringInsertIterator.insertConstStringInstruction(
-                appView, code, appView.dexItemFactory().createString(replacement));
-      }
+      Value value = insertStringConstantInstruction(appView, code, iterator, invoke, replacement);
       iterator.next();
       DexMethod invokedMethod = invoke.getInvokedMethod();
       if (invoke.isInvokeConstructor(appView.dexItemFactory())) {
@@ -151,16 +132,6 @@
           || factory.stringBuilderMethods.isAppendStringMethod(method);
     }
 
-    private DexMethod getConstructorWithStringParameter(
-        DexMethod invokedMethod, DexItemFactory factory) {
-      if (invokedMethod.getHolderType() == factory.stringBufferType) {
-        return factory.stringBufferMethods.stringConstructor;
-      } else {
-        assert invokedMethod.getHolderType() == factory.stringBuilderType;
-        return factory.stringBuilderMethods.stringConstructor;
-      }
-    }
-
     private DexMethod getAppendWithStringParameter(
         DexMethod invokedMethod, DexItemFactory factory) {
       if (invokedMethod.getHolderType() == factory.stringBufferType) {
@@ -171,4 +142,128 @@
       }
     }
   }
+
+  class ReplaceByExistingString implements StringBuilderAction {
+
+    private final Value existingString;
+
+    public ReplaceByExistingString(Value existingString) {
+      assert existingString.isNeverNull();
+      this.existingString = existingString;
+    }
+
+    @Override
+    public void perform(
+        AppView<?> appView,
+        IRCode code,
+        InstructionListIterator iterator,
+        Instruction instruction,
+        StringBuilderOracle oracle) {
+      instruction.outValue().replaceUsers(existingString);
+      iterator.removeOrReplaceByDebugLocalRead();
+    }
+  }
+
+  class ReplaceByStringConcat implements StringBuilderAction {
+
+    private final Value first;
+    private final Value second;
+
+    private final String newConstant;
+
+    private ReplaceByStringConcat(Value first, Value second, String newConstant) {
+      assert first != null || newConstant != null;
+      assert second != null || newConstant != null;
+      this.first = first;
+      this.second = second;
+      this.newConstant = newConstant;
+    }
+
+    public static ReplaceByStringConcat replaceByValues(Value first, Value second) {
+      return new ReplaceByStringConcat(first, second, null);
+    }
+
+    public static ReplaceByStringConcat replaceByNewConstantConcatValue(
+        String newConstant, Value second) {
+      return new ReplaceByStringConcat(null, second, newConstant);
+    }
+
+    public static ReplaceByStringConcat replaceByValueConcatNewConstant(
+        Value first, String newConstant) {
+      return new ReplaceByStringConcat(first, null, newConstant);
+    }
+
+    @Override
+    public void perform(
+        AppView<?> appView,
+        IRCode code,
+        InstructionListIterator iterator,
+        Instruction instruction,
+        StringBuilderOracle oracle) {
+      Value constString = null;
+      if (newConstant != null) {
+        Instruction previous = iterator.previous();
+        constString =
+            insertStringConstantInstruction(appView, code, iterator, previous, newConstant);
+        iterator.next();
+      }
+      assert first != null || constString != null;
+      assert second != null || constString != null;
+      // To ensure that we do not fail narrowing when evaluating String.concat, we mark the type
+      // as maybe null.
+      Value newOutValue =
+          code.createValue(
+              TypeElement.stringClassType(appView, Nullability.maybeNull()),
+              instruction.getLocalInfo());
+      iterator.replaceCurrentInstruction(
+          InvokeVirtual.builder()
+              .setOutValue(newOutValue)
+              .setMethod(appView.dexItemFactory().stringMembers.concat)
+              .setArguments(
+                  ImmutableList.of(
+                      first != null ? first : constString, second != null ? second : constString))
+              .build());
+    }
+  }
+
+  static DexMethod getConstructorWithStringParameter(
+      DexMethod invokedMethod, DexItemFactory factory) {
+    if (invokedMethod.getHolderType() == factory.stringBufferType) {
+      return factory.stringBufferMethods.stringConstructor;
+    } else {
+      assert invokedMethod.getHolderType() == factory.stringBuilderType;
+      return factory.stringBuilderMethods.stringConstructor;
+    }
+  }
+
+  static Value insertStringConstantInstruction(
+      AppView<?> appView,
+      IRCode code,
+      InstructionListIterator iterator,
+      Instruction instruction,
+      String newString) {
+    // If the block has catch handlers, inserting a constant string in the same block as the
+    // append violates our block representation in DEX since constant string is throwing. If the
+    // append is in a block with catch handlers, we simply insert a new constant string in the
+    // entry block after all arguments.
+    Value value;
+    if (!instruction.getBlock().hasCatchHandlers()) {
+      value =
+          iterator.insertConstStringInstruction(
+              appView, code, appView.dexItemFactory().createString(newString));
+    } else {
+      InstructionListIterator stringInsertIterator = code.entryBlock().listIterator(code);
+      while (stringInsertIterator.hasNext()) {
+        Instruction next = stringInsertIterator.next();
+        if (!next.isArgument()) {
+          stringInsertIterator.previous();
+          break;
+        }
+      }
+      value =
+          stringInsertIterator.insertConstStringInstruction(
+              appView, code, appView.dexItemFactory().createString(newString));
+    }
+    return value;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java
index ee85c13..93c004f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java
@@ -33,7 +33,7 @@
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.EscapeNode;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.ImplicitToStringNode;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.InitNode;
-import com.android.tools.r8.ir.optimize.string.StringBuilderNode.InitOrAppend;
+import com.android.tools.r8.ir.optimize.string.StringBuilderNode.InitOrAppendNode;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.LoopNode;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.NewInstanceNode;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.SplitReferenceNode;
@@ -273,7 +273,13 @@
               Value receiver = invoke.getReceiver();
               if (oracle.isInit(instruction)) {
                 InitNode initNode = createInitNode(instruction.asInvokeDirect());
-                initNode.setConstantArgument(oracle.getConstantArgument(instruction));
+                String constantArgument = oracle.getConstantArgument(instruction);
+                initNode.setConstantArgument(constantArgument);
+                if (constantArgument == null
+                    && oracle.isStringConstructor(instruction)
+                    && invoke.getFirstNonReceiverArgument().isNeverNull()) {
+                  initNode.setNonConstantArgument(invoke.getFirstNonReceiverArgument());
+                }
                 if (invoke.arguments().size() == 2) {
                   Value arg = invoke.getOperand(1);
                   if (oracle.hasStringBuilderType(arg)) {
@@ -288,9 +294,15 @@
                     escaped -> nodeConsumer.accept(escaped, createInspectionNode(instruction)));
               } else if (oracle.isAppend(instruction)) {
                 AppendNode appendNode = createAppendNode(instruction.asInvokeVirtual());
-                appendNode.setConstantArgument(oracle.getConstantArgument(instruction));
-                Value arg = invoke.getFirstNonReceiverArgument().getAliasedValue();
-                if (oracle.hasStringBuilderType(arg)) {
+                String constantArgument = oracle.getConstantArgument(instruction);
+                appendNode.setConstantArgument(constantArgument);
+                Value arg = invoke.getFirstNonReceiverArgument();
+                if (constantArgument == null
+                    && oracle.isAppendString(instruction)
+                    && arg.isNeverNull()) {
+                  appendNode.setNonConstantArgument(arg);
+                }
+                if (oracle.hasStringBuilderType(arg.getAliasedValue())) {
                   insertImplicitToStringNode(
                       arg, instruction, appendNode, escapeState, nodeConsumer);
                 }
@@ -341,7 +353,7 @@
           private void insertImplicitToStringNode(
               Value value,
               Instruction instruction,
-              InitOrAppend node,
+              InitOrAppendNode node,
               StringBuilderEscapeState escapeState,
               BiConsumer<Value, StringBuilderNode> nodeConsumer) {
             assert escapeState.isLiveStringBuilder(value);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderNode.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderNode.java
index c189299..5e01833 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderNode.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderNode.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.InvokeVirtual;
 import com.android.tools.r8.ir.code.NewInstance;
+import com.android.tools.r8.ir.code.Value;
 import com.google.common.collect.Sets;
 import java.util.Set;
 
@@ -27,7 +28,15 @@
     StringBuilderInstruction asStringBuilderInstructionNode();
   }
 
-  interface InitOrAppend extends StringBuilderInstruction {
+  interface InitOrAppendNode extends StringBuilderInstruction {
+
+    boolean isInitNode();
+
+    boolean isAppendNode();
+
+    InitNode asInitNode();
+
+    AppendNode asAppendNode();
 
     boolean hasConstantArgument();
 
@@ -38,6 +47,22 @@
     void setImplicitToStringNode(ImplicitToStringNode node);
 
     ImplicitToStringNode getImplicitToStringNode();
+
+    boolean hasNonConstantArgument();
+
+    Value getNonConstantArgument();
+
+    default boolean hasConstantOrNonConstantArgument() {
+      return hasConstantArgument() || hasNonConstantArgument();
+    }
+
+    boolean hasSingleSuccessor();
+
+    boolean hasSinglePredecessor();
+
+    StringBuilderNode getSingleSuccessor();
+
+    StringBuilderNode getSinglePredecessor();
   }
 
   private final Set<StringBuilderNode> successors = Sets.newLinkedHashSet();
@@ -69,11 +94,11 @@
     return false;
   }
 
-  boolean isInitNode() {
+  public boolean isInitNode() {
     return false;
   }
 
-  boolean isAppendNode() {
+  public boolean isAppendNode() {
     return false;
   }
 
@@ -105,15 +130,15 @@
     return null;
   }
 
-  InitNode asInitNode() {
+  public InitNode asInitNode() {
     return null;
   }
 
-  AppendNode asAppendNode() {
+  public AppendNode asAppendNode() {
     return null;
   }
 
-  InitOrAppend asInitOrAppend() {
+  InitOrAppendNode asInitOrAppend() {
     return null;
   }
 
@@ -141,11 +166,11 @@
     return isDead;
   }
 
-  boolean hasSingleSuccessor() {
+  public boolean hasSingleSuccessor() {
     return successors.size() == 1;
   }
 
-  StringBuilderNode getSingleSuccessor() {
+  public StringBuilderNode getSingleSuccessor() {
     assert hasSingleSuccessor();
     return successors.iterator().next();
   }
@@ -159,11 +184,11 @@
     return successors;
   }
 
-  boolean hasSinglePredecessor() {
+  public boolean hasSinglePredecessor() {
     return predecessors.size() == 1;
   }
 
-  StringBuilderNode getSinglePredecessor() {
+  public StringBuilderNode getSinglePredecessor() {
     assert hasSinglePredecessor();
     return predecessors.iterator().next();
   }
@@ -278,18 +303,19 @@
 
   /** An initNode is where a StringBuilder/StringBuffer is initialized. */
   static class InitNode extends StringBuilderNode
-      implements InitOrAppend, StringBuilderInstruction {
+      implements InitOrAppendNode, StringBuilderInstruction {
 
     private final InvokeDirect instruction;
     private ImplicitToStringNode implicitToStringNode;
     private String constantArgument;
+    private Value nonConstantArgument;
 
     private InitNode(InvokeDirect instruction) {
       this.instruction = instruction;
     }
 
     @Override
-    boolean isInitNode() {
+    public boolean isInitNode() {
       return true;
     }
 
@@ -299,12 +325,12 @@
     }
 
     @Override
-    InitNode asInitNode() {
+    public InitNode asInitNode() {
       return this;
     }
 
     @Override
-    InitOrAppend asInitOrAppend() {
+    InitOrAppendNode asInitOrAppend() {
       return this;
     }
 
@@ -329,16 +355,6 @@
     }
 
     @Override
-    public void setImplicitToStringNode(ImplicitToStringNode node) {
-      implicitToStringNode = node;
-    }
-
-    @Override
-    public ImplicitToStringNode getImplicitToStringNode() {
-      return implicitToStringNode;
-    }
-
-    @Override
     public String getConstantArgument() {
       return constantArgument;
     }
@@ -347,22 +363,53 @@
     public boolean hasConstantArgument() {
       return constantArgument != null;
     }
+
+    public void setNonConstantArgument(Value value) {
+      assert value.isNeverNull();
+      this.nonConstantArgument = value;
+    }
+
+    @Override
+    public void setImplicitToStringNode(ImplicitToStringNode node) {
+      implicitToStringNode = node;
+    }
+
+    @Override
+    public ImplicitToStringNode getImplicitToStringNode() {
+      return implicitToStringNode;
+    }
+
+    @Override
+    public boolean hasNonConstantArgument() {
+      return nonConstantArgument != null;
+    }
+
+    @Override
+    public Value getNonConstantArgument() {
+      assert nonConstantArgument != null;
+      return nonConstantArgument;
+    }
+
+    boolean isConstructorInvokeSideEffectFree(StringBuilderOracle oracle) {
+      return oracle.isConstructorInvokeSideEffectFree(instruction);
+    }
   }
 
   /** AppendNodes are StringBuilder.append or StringBuffer.append. */
   static class AppendNode extends StringBuilderNode
-      implements InitOrAppend, StringBuilderInstruction {
+      implements InitOrAppendNode, StringBuilderInstruction {
 
     private final InvokeVirtual instruction;
     private ImplicitToStringNode implicitToStringNode;
     private String constantArgument;
+    private Value nonConstantArgument;
 
     private AppendNode(InvokeVirtual instruction) {
       this.instruction = instruction;
     }
 
     @Override
-    boolean isAppendNode() {
+    public boolean isAppendNode() {
       return true;
     }
 
@@ -372,12 +419,12 @@
     }
 
     @Override
-    AppendNode asAppendNode() {
+    public AppendNode asAppendNode() {
       return this;
     }
 
     @Override
-    InitOrAppend asInitOrAppend() {
+    InitOrAppendNode asInitOrAppend() {
       return this;
     }
 
@@ -412,6 +459,21 @@
     }
 
     @Override
+    public boolean hasNonConstantArgument() {
+      return nonConstantArgument != null;
+    }
+
+    public void setNonConstantArgument(Value value) {
+      assert value.isNeverNull();
+      this.nonConstantArgument = value;
+    }
+
+    @Override
+    public Value getNonConstantArgument() {
+      return nonConstantArgument;
+    }
+
+    @Override
     public String getConstantArgument() {
       return constantArgument;
     }
@@ -540,13 +602,13 @@
    */
   static class ImplicitToStringNode extends StringBuilderNode {
 
-    private final InitOrAppend initOrAppend;
+    private final InitOrAppendNode initOrAppend;
 
-    ImplicitToStringNode(InitOrAppend initOrAppend) {
+    ImplicitToStringNode(InitOrAppendNode initOrAppend) {
       this.initOrAppend = initOrAppend;
     }
 
-    public InitOrAppend getInitOrAppend() {
+    public InitOrAppendNode getInitOrAppend() {
       return initOrAppend;
     }
 
@@ -601,7 +663,7 @@
     return new OtherStringBuilderNode(instruction);
   }
 
-  static ImplicitToStringNode createImplicitToStringNode(InitOrAppend otherNode) {
+  static ImplicitToStringNode createImplicitToStringNode(InitOrAppendNode otherNode) {
     return new ImplicitToStringNode(otherNode);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderNodeMuncher.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderNodeMuncher.java
index 0659a8a..0fe609e 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderNodeMuncher.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderNodeMuncher.java
@@ -9,14 +9,18 @@
 import com.android.tools.r8.ir.optimize.string.StringBuilderAction.AppendWithNewConstantString;
 import com.android.tools.r8.ir.optimize.string.StringBuilderAction.RemoveStringBuilderAction;
 import com.android.tools.r8.ir.optimize.string.StringBuilderAction.ReplaceByConstantString;
+import com.android.tools.r8.ir.optimize.string.StringBuilderAction.ReplaceByExistingString;
+import com.android.tools.r8.ir.optimize.string.StringBuilderAction.ReplaceByStringConcat;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.AppendNode;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.ImplicitToStringNode;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.InitNode;
-import com.android.tools.r8.ir.optimize.string.StringBuilderNode.InitOrAppend;
+import com.android.tools.r8.ir.optimize.string.StringBuilderNode.InitOrAppendNode;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.NewInstanceNode;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.StringBuilderInstruction;
 import com.android.tools.r8.ir.optimize.string.StringBuilderNode.ToStringNode;
 import com.android.tools.r8.utils.WorkList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
@@ -67,6 +71,14 @@
     public boolean isLooping(StringBuilderNode root) {
       return looping.contains(root);
     }
+
+    public boolean isEscaping(StringBuilderNode root) {
+      return escaping.contains(root);
+    }
+
+    public boolean isInspecting(StringBuilderNode root) {
+      return inspectingCapacity.contains(root);
+    }
   }
 
   private interface PeepholePattern {
@@ -93,30 +105,32 @@
     @Override
     public boolean optimize(
         StringBuilderNode root, StringBuilderNode currentNode, MunchingState munchingState) {
-      if (!currentNode.isAppendNode()) {
+      AppendNode appendNode = currentNode.asAppendNode();
+      if (appendNode == null || !appendNode.hasSinglePredecessor()) {
         return false;
       }
-      String currentConstantArgument = getConstantArgumentForNode(currentNode, munchingState);
-      if (currentConstantArgument == null || !currentNode.hasSinglePredecessor()) {
-        return false;
-      }
-      StringBuilderNode previous = currentNode.getSinglePredecessor();
-      String previousConstantArgument = getConstantArgumentForNode(previous, munchingState);
-      if (previousConstantArgument == null || !previous.hasSingleSuccessor()) {
+      InitOrAppendNode previous = currentNode.getSinglePredecessor().asInitOrAppend();
+      if (previous == null || !previous.hasSingleSuccessor()) {
         return false;
       }
       // The capacity changes based on the init call (on JVM it adds 16 to length of input).
       if (previous.isInitNode() && munchingState.inspectingCapacity.contains(root)) {
         return false;
       }
-      assert previous.isInitOrAppend();
+      String currentConstantArgument = getConstantArgumentForNode(appendNode, munchingState);
+      if (currentConstantArgument == null) {
+        return false;
+      }
+      String previousConstantArgument = getConstantArgumentForNode(previous, munchingState);
+      if (previousConstantArgument == null) {
+        return false;
+      }
       String newConstant = previousConstantArgument + currentConstantArgument;
-      InitOrAppend initOrAppend = previous.asInitOrAppend();
-      initOrAppend.setConstantArgument(newConstant);
+      previous.setConstantArgument(newConstant);
       munchingState.actions.put(
-          initOrAppend.getInstruction(), new AppendWithNewConstantString(newConstant));
+          previous.getInstruction(), new AppendWithNewConstantString(newConstant));
       munchingState.actions.put(
-          currentNode.asAppendNode().getInstruction(), RemoveStringBuilderAction.getInstance());
+          appendNode.getInstruction(), RemoveStringBuilderAction.getInstance());
       currentNode.removeNode();
       return true;
     }
@@ -126,13 +140,13 @@
    * This peephole will try to remove toString nodes and replace by a constant string:
    *
    * <pre>
-   * newInstance -> init("foo") -> append("bar") -> toString() =>
-   * newInstance -> init("foo") -> append("bar")
+   * newInstance -> init("foo") -> toString() => newInstance -> init("foo") -> append("bar")
+   * actions: [toString() => ReplaceByConstantString("foo")]
    * </pre>
    *
    * <p>If the node is an implicitToString, we update the append of another builder to have the new
-   * constant value directly. If not, we keep track of the outValue toString() had is replaced by a
-   * constant, by updating {@code MunchingState.optimizedStrings}
+   * constant value directly. If not, we keep track of the outValue toString() being replaced by a
+   * constant by updating {@code MunchingState.optimizedStrings}
    */
   private static class MunchToString implements PeepholePattern {
 
@@ -141,6 +155,9 @@
         StringBuilderNode originalRoot,
         StringBuilderNode currentNode,
         MunchingState munchingState) {
+      if (munchingState.isEscaping(originalRoot) || munchingState.isInspecting(originalRoot)) {
+        return false;
+      }
       if (!currentNode.isToStringNode() && !currentNode.isImplicitToStringNode()) {
         return false;
       }
@@ -148,48 +165,127 @@
       if (newInstanceNode == null || !newInstanceNode.hasSingleSuccessor()) {
         return false;
       }
-      StringBuilderNode init = newInstanceNode.getSingleSuccessor();
-      String rootConstantArgument = getConstantArgumentForNode(init, munchingState);
-      if (rootConstantArgument == null || !init.isInitNode()) {
+      InitNode init = newInstanceNode.getSingleSuccessor().asInitNode();
+      if (init == null || !init.hasSingleSuccessor()) {
         return false;
       }
-      // This is either <init>(str) -> toString() or <init>(str) -> append(str) -> toString()
+      if (!currentNode.hasSinglePredecessor() || currentNode.getSinglePredecessor() != init) {
+        return false;
+      }
+      String initConstantArgument = getConstantArgumentForNode(init, munchingState);
+      if (initConstantArgument == null) {
+        return false;
+      }
       // If the string builder dependency is directly given to another string builder, there is
       // no toString() but an append with this string builder as argument.
-      if (!currentNode.hasSinglePredecessor() || !init.hasSingleSuccessor()) {
-        return false;
-      }
-      String constantArgument = null;
-      if (currentNode.getSinglePredecessor() == init) {
-        constantArgument = rootConstantArgument;
-      } else {
-        StringBuilderNode expectedAppend = init.getSingleSuccessor();
-        StringBuilderNode expectedSameAppend = currentNode.getSinglePredecessor();
-        String appendConstantArgument = getConstantArgumentForNode(expectedAppend, munchingState);
-        if (expectedAppend == expectedSameAppend && appendConstantArgument != null) {
-          // TODO(b/190489514): See if this larger pattern is necessary.
-          assert false : "See why this larger pattern is necessary";
-          constantArgument = rootConstantArgument + appendConstantArgument;
-        }
-      }
-      if (constantArgument == null) {
-        return false;
-      }
       if (currentNode.isToStringNode()) {
         ToStringNode toStringNode = currentNode.asToStringNode();
         munchingState.actions.put(
-            toStringNode.getInstruction(), new ReplaceByConstantString(constantArgument));
+            toStringNode.getInstruction(), new ReplaceByConstantString(initConstantArgument));
         String oldValue =
             munchingState.optimizedStrings.put(
-                toStringNode.getInstruction().outValue(), constantArgument);
+                toStringNode.getInstruction().outValue(), initConstantArgument);
         assert oldValue == null;
       } else {
         assert currentNode.isImplicitToStringNode();
         ImplicitToStringNode implicitToStringNode = currentNode.asImplicitToStringNode();
-        InitOrAppend initOrAppend = implicitToStringNode.getInitOrAppend();
-        initOrAppend.setConstantArgument(constantArgument);
+        InitOrAppendNode initOrAppend = implicitToStringNode.getInitOrAppend();
+        initOrAppend.setConstantArgument(initConstantArgument);
         munchingState.actions.put(
-            initOrAppend.getInstruction(), new AppendWithNewConstantString(constantArgument));
+            initOrAppend.getInstruction(), new AppendWithNewConstantString(initConstantArgument));
+      }
+      munchingState.materializingInstructions.get(originalRoot).remove(currentNode);
+      currentNode.removeNode();
+      return true;
+    }
+  }
+
+  /**
+   * This peephole will try to remove toString nodes and replace by an invoke to String.concat:
+   *
+   * <pre>
+   * newInstance -> init(notNull(string)) -> append(notNull(otherString)) -> toString() =>
+   * newInstance -> init(notNull(string)) -> append(otherString)
+   * actions: [toString() => string.concat(otherString)]
+   * </pre>
+   *
+   * <p>This pattern only triggers when a constant munching of toString could happen.
+   */
+  private static class MunchToStringIntoStringConcat implements PeepholePattern {
+
+    @Override
+    public boolean optimize(
+        StringBuilderNode originalRoot,
+        StringBuilderNode currentNode,
+        MunchingState munchingState) {
+      if (munchingState.isEscaping(originalRoot) || munchingState.isInspecting(originalRoot)) {
+        return false;
+      }
+      // TODO(b/129200243): Handle implicit tostring nodes.
+      if (!currentNode.isToStringNode() || !currentNode.hasSinglePredecessor()) {
+        return false;
+      }
+      StringBuilderNode root = findFirstNonSentinelRoot(originalRoot);
+      if (!root.isNewInstanceNode() || !root.hasSingleSuccessor()) {
+        return false;
+      }
+      InitOrAppendNode firstNode = root.getSingleSuccessor().asInitNode();
+      if (firstNode == null || !firstNode.hasSingleSuccessor()) {
+        return false;
+      }
+      if (firstNode.asInitNode().isConstructorInvokeSideEffectFree(munchingState.oracle)
+          && "".equals(firstNode.getConstantArgument())
+          && firstNode.hasSingleSuccessor()) {
+        firstNode = firstNode.getSingleSuccessor().asAppendNode();
+        if (firstNode == null
+            || !firstNode.hasSinglePredecessor()
+            || !firstNode.hasSingleSuccessor()) {
+          return false;
+        }
+      }
+      // We cannot String.concat or return the string safely when it is not constant and maybe null.
+      if (!firstNode.hasConstantOrNonConstantArgument()) {
+        return false;
+      }
+      List<InitOrAppendNode> initOrAppends = Lists.newArrayList(firstNode);
+      if (currentNode.getSinglePredecessor() != firstNode) {
+        AppendNode appendAfterFirstNode = firstNode.getSingleSuccessor().asAppendNode();
+        AppendNode appendBeforeToString = currentNode.getSinglePredecessor().asAppendNode();
+        if (appendAfterFirstNode == null
+            || appendAfterFirstNode != appendBeforeToString
+            || !appendAfterFirstNode.hasConstantOrNonConstantArgument()) {
+          return false;
+        }
+        initOrAppends.add(appendAfterFirstNode);
+      }
+      // Check that all values are not constant otherwise we can compute the constant value and
+      // replace all entirely.
+      if (Iterables.all(initOrAppends, InitOrAppendNode::hasConstantArgument)) {
+        return false;
+      }
+      // Replace with the string itself.
+      Instruction currentInstruction = currentNode.asToStringNode().getInstruction();
+      InitOrAppendNode first = initOrAppends.get(0);
+      if (initOrAppends.size() == 1) {
+        munchingState.actions.put(
+            currentInstruction, new ReplaceByExistingString(first.getNonConstantArgument()));
+      } else {
+        InitOrAppendNode second = initOrAppends.get(1);
+        ReplaceByStringConcat concatAction;
+        if (first.hasConstantArgument()) {
+          concatAction =
+              ReplaceByStringConcat.replaceByNewConstantConcatValue(
+                  first.getConstantArgument(), second.getNonConstantArgument());
+        } else if (second.hasConstantArgument()) {
+          concatAction =
+              ReplaceByStringConcat.replaceByValueConcatNewConstant(
+                  first.getNonConstantArgument(), second.getConstantArgument());
+        } else {
+          concatAction =
+              ReplaceByStringConcat.replaceByValues(
+                  first.getNonConstantArgument(), second.getNonConstantArgument());
+        }
+        munchingState.actions.put(currentInstruction, concatAction);
       }
       munchingState.materializingInstructions.get(originalRoot).remove(currentNode);
       currentNode.removeNode();
@@ -198,21 +294,11 @@
   }
 
   private static String getConstantArgumentForNode(
-      StringBuilderNode node, MunchingState munchingState) {
-    if (node.isAppendNode()) {
-      AppendNode appendNode = node.asAppendNode();
-      if (appendNode.hasConstantArgument()) {
-        return appendNode.getConstantArgument();
-      }
-      return getOptimizedConstantArgument(appendNode, munchingState);
-    } else if (node.isInitNode()) {
-      InitNode initNode = node.asInitNode();
-      if (initNode.hasConstantArgument()) {
-        return initNode.getConstantArgument();
-      }
-      return getOptimizedConstantArgument(initNode, munchingState);
+      InitOrAppendNode node, MunchingState munchingState) {
+    if (node.hasConstantArgument()) {
+      return node.getConstantArgument();
     }
-    return null;
+    return getOptimizedConstantArgument(node, munchingState);
   }
 
   private static String getOptimizedConstantArgument(
@@ -262,7 +348,6 @@
             removeNode = true;
           }
         } else if (currentNode.isInitNode()
-            && currentNode.asInitNode().hasConstantArgument()
             && currentNode.hasSinglePredecessor()
             && currentNode.getSinglePredecessor().isNewInstanceNode()
             && currentNode.getSuccessors().isEmpty()
@@ -312,7 +397,12 @@
   }
 
   private static final PeepholePattern[] peepholePatterns =
-      new PeepholePattern[] {new MunchAppends(), new MunchToString(), new MunchNonMaterializing()};
+      new PeepholePattern[] {
+        new MunchAppends(),
+        new MunchToString(),
+        new MunchToStringIntoStringConcat(),
+        new MunchNonMaterializing()
+      };
 
   static boolean optimize(
       StringBuilderNode root, StringBuilderNode currentNode, MunchingState munchingState) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOracle.java
index 063f791..8b59c2f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOracle.java
@@ -43,6 +43,12 @@
 
   boolean isInit(Instruction instruction);
 
+  boolean isAppendString(Instruction instruction);
+
+  boolean isStringConstructor(Instruction instruction);
+
+  boolean isConstructorInvokeSideEffectFree(Instruction instruction);
+
   class DefaultStringBuilderOracle implements StringBuilderOracle {
 
     private final DexItemFactory factory;
@@ -185,12 +191,12 @@
       }
       DexMethod invokedMethod = instruction.asInvokeMethod().getInvokedMethod();
       if (factory.stringBuilderMethods.isAppendObjectOrCharSequenceMethod(invokedMethod)
-          || factory.stringBufferMethods.isAppendObjectOrCharSequenceMethod(invokedMethod)
-          || factory.stringBuilderMethods.charSequenceConstructor == invokedMethod
-          || factory.stringBufferMethods.charSequenceConstructor == invokedMethod) {
-        assert instruction.inValues().size() == 2;
+          || factory.stringBufferMethods.isAppendObjectOrCharSequenceMethod(invokedMethod)) {
         return !instruction.inValues().get(1).getType().isStringType(factory);
       }
+      if (invokedMethod.isInstanceInitializer(factory)) {
+        return !isConstructorInvokeSideEffectFree(instruction);
+      }
       if (factory.stringBuilderMethods.isAppendCharArrayMethod(invokedMethod)
           || factory.stringBufferMethods.isAppendCharArrayMethod(invokedMethod)) {
         return instruction.asInvokeVirtual().getFirstNonReceiverArgument().isMaybeNull();
@@ -207,5 +213,41 @@
       return factory.stringBuilderMethods.isConstructorMethod(invokedMethod)
           || factory.stringBufferMethods.isConstructorMethod(invokedMethod);
     }
+
+    @Override
+    public boolean isAppendString(Instruction instruction) {
+      if (!instruction.isInvokeMethod()) {
+        return false;
+      }
+      DexMethod invokedMethod = instruction.asInvokeMethod().getInvokedMethod();
+      return factory.stringBuilderMethods.isAppendStringMethod(invokedMethod)
+          || factory.stringBufferMethods.isAppendStringMethod(invokedMethod);
+    }
+
+    @Override
+    public boolean isStringConstructor(Instruction instruction) {
+      if (!instruction.isInvokeMethod()) {
+        return false;
+      }
+      DexMethod invokedMethod = instruction.asInvokeMethod().getInvokedMethod();
+      return invokedMethod == factory.stringBuilderMethods.stringConstructor
+          || invokedMethod == factory.stringBufferMethods.stringConstructor;
+    }
+
+    @Override
+    public boolean isConstructorInvokeSideEffectFree(Instruction instruction) {
+      if (!instruction.isInvokeConstructor(factory)) {
+        return false;
+      }
+      DexMethod invokedMethod = instruction.asInvokeMethod().getInvokedMethod();
+      if (invokedMethod.getHolderType() == factory.stringBuilderType) {
+        return factory.stringBuilderMethods.constructorInvokeIsSideEffectFree(
+            invokedMethod, instruction.inValues());
+      } else {
+        assert invokedMethod.getHolderType() == factory.stringBufferType;
+        return factory.stringBufferMethods.constructorInvokeIsSideEffectFree(
+            invokedMethod, instruction.inValues());
+      }
+    }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatAppendNotNullAppendConstantAppendNotNullTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatAppendNotNullAppendConstantAppendNotNullTest.java
index d4ac494..4646691 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatAppendNotNullAppendConstantAppendNotNullTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatAppendNotNullAppendConstantAppendNotNullTest.java
@@ -53,7 +53,6 @@
               MethodSubject methodSubject = inspect.clazz(Main.class).mainMethod();
               assertThat(methodSubject, isPresent());
               assertEquals(1, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
-              // TODO(b/129200243): Should only have two appends.
               assertEquals(3, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
             });
   }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatAppendNotNullAppendNotNullTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatAppendNotNullAppendNotNullTest.java
index c28729a..cbe8fa7 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatAppendNotNullAppendNotNullTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatAppendNotNullAppendNotNullTest.java
@@ -13,6 +13,7 @@
 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.CodeMatchers;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -52,9 +53,9 @@
             inspect -> {
               MethodSubject methodSubject = inspect.clazz(Main.class).mainMethod();
               assertThat(methodSubject, isPresent());
-              // TODO(b/129200243): Should be String.concat.
-              assertEquals(1, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
-              assertEquals(2, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
+              assertEquals(0, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
+              assertEquals(0, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
+              assertThat(methodSubject, CodeMatchers.invokesMethodWithName("concat"));
             });
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatAppendNotNullTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatAppendNotNullTest.java
index d2c95ab..8005ed5 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatAppendNotNullTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatAppendNotNullTest.java
@@ -7,12 +7,14 @@
 import static com.android.tools.r8.ir.optimize.string.utils.StringBuilderCodeMatchers.countStringBuilderAppends;
 import static com.android.tools.r8.ir.optimize.string.utils.StringBuilderCodeMatchers.countStringBuilderInits;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 
 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.CodeMatchers;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -52,9 +54,9 @@
             inspect -> {
               MethodSubject methodSubject = inspect.clazz(Main.class).mainMethod();
               assertThat(methodSubject, isPresent());
-              // TODO(b/129200243): This should just be arg.toString()
-              assertEquals(1, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
-              assertEquals(1, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
+              assertEquals(0, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
+              assertEquals(0, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
+              assertThat(methodSubject, not(CodeMatchers.invokesMethodWithName("concat")));
             });
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatConstantInitTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatConstantInitTest.java
index 9b9280f..0f7d286 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatConstantInitTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatConstantInitTest.java
@@ -13,6 +13,7 @@
 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.CodeMatchers;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -52,9 +53,9 @@
             inspect -> {
               MethodSubject methodSubject = inspect.clazz(Main.class).mainMethod();
               assertThat(methodSubject, isPresent());
-              // TODO(b/129200243): Should be String.concat.
-              assertEquals(1, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
-              assertEquals(1, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
+              assertEquals(0, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
+              assertEquals(0, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
+              assertThat(methodSubject, CodeMatchers.invokesMethodWithName("concat"));
             });
   }
 
@@ -63,7 +64,7 @@
     public static void main(String[] args) {
       String arg = System.currentTimeMillis() > 0 ? "o" : null;
       if (arg != null) {
-        // The optimization should join "ok" into init and then append with arg.
+        // The optimization should join "ok" into init and then concat with arg.
         System.out.println(new StringBuilder("o").append("k").append(arg).toString());
       }
     }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatInitNotNullAppendNotNullTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatInitNotNullAppendNotNullTest.java
index 4dce8fd..aca301c 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatInitNotNullAppendNotNullTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatInitNotNullAppendNotNullTest.java
@@ -13,6 +13,7 @@
 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.CodeMatchers;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -52,9 +53,9 @@
             inspect -> {
               MethodSubject methodSubject = inspect.clazz(Main.class).mainMethod();
               assertThat(methodSubject, isPresent());
-              // TODO(b/129200243): Should be concat.
-              assertEquals(1, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
-              assertEquals(1, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
+              assertEquals(0, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
+              assertEquals(0, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
+              assertThat(methodSubject, CodeMatchers.invokesMethodWithName("concat"));
             });
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatInitNotNullTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatInitNotNullTest.java
index 690a604..4bb550e 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatInitNotNullTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatInitNotNullTest.java
@@ -7,12 +7,14 @@
 import static com.android.tools.r8.ir.optimize.string.utils.StringBuilderCodeMatchers.countStringBuilderAppends;
 import static com.android.tools.r8.ir.optimize.string.utils.StringBuilderCodeMatchers.countStringBuilderInits;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 
 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.CodeMatchers;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -52,9 +54,9 @@
             inspect -> {
               MethodSubject methodSubject = inspect.clazz(Main.class).mainMethod();
               assertThat(methodSubject, isPresent());
-              // TODO(b/129200243): This should just be the string as is.
-              assertEquals(1, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
+              assertEquals(0, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
               assertEquals(0, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
+              assertThat(methodSubject, not(CodeMatchers.invokesMethodWithName("concat")));
             });
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatNoRewriteConstantsTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatNoRewriteConstantsTest.java
index 73e61bc..33320dc 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatNoRewriteConstantsTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatNoRewriteConstantsTest.java
@@ -7,12 +7,14 @@
 import static com.android.tools.r8.ir.optimize.string.utils.StringBuilderCodeMatchers.countStringBuilderAppends;
 import static com.android.tools.r8.ir.optimize.string.utils.StringBuilderCodeMatchers.countStringBuilderInits;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 
 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.CodeMatchers;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -54,6 +56,7 @@
               assertThat(methodSubject, isPresent());
               assertEquals(0, countStringBuilderInits(methodSubject.asFoundMethodSubject()));
               assertEquals(0, countStringBuilderAppends(methodSubject.asFoundMethodSubject()));
+              assertThat(methodSubject, not(CodeMatchers.invokesMethodWithName("concat")));
             });
   }