Disable removal of append with substring when unused

Bug: b/380182105
Change-Id: I039bf72226358b4b7ceb3157d0e1339d47148625
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 5cdcc6b..89469c7 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
@@ -385,13 +385,16 @@
           boolean canRemoveIfLastAndNoLoop =
               !isLoopingOnPath(root, currentNode, munchingState)
                   && currentNode.getSuccessors().isEmpty();
+          Instruction instruction = currentNode.asAppendNode().getInstruction();
           boolean hasKnownArgumentOrCannotBeObserved =
               appendNode.hasConstantOrNonConstantArgument()
-                  || !munchingState.oracle.canObserveStringBuilderCall(
-                      currentNode.asAppendNode().getInstruction());
+                  || !munchingState.oracle.canObserveStringBuilderCall(instruction);
+          // R8 would need to check for range overflow if removing append with sub arrays.
+          boolean canRemoveNonSub = !munchingState.oracle.isAppendWithSubArray(instruction);
           if (canRemoveIfNoInspectionOrMaterializing
               && canRemoveIfLastAndNoLoop
-              && hasKnownArgumentOrCannotBeObserved) {
+              && hasKnownArgumentOrCannotBeObserved
+              && canRemoveNonSub) {
             removeNode = true;
           }
         } else if (currentNode.isInitNode()
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 2c43b60..d9428ac 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
@@ -12,7 +12,6 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InvokeDirect;
-import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
 import com.android.tools.r8.ir.code.InvokeVirtual;
 import com.android.tools.r8.ir.code.Value;
 import java.util.List;
@@ -39,6 +38,8 @@
 
   boolean isAppend(Instruction instruction);
 
+  boolean isAppendWithSubArray(Instruction instruction);
+
   boolean canObserveStringBuilderCall(Instruction instruction);
 
   boolean isInit(Instruction instruction);
@@ -182,7 +183,11 @@
           || factory.stringBufferMethods.isAppendMethod(invokedMethod);
     }
 
-    public boolean isAppendWithSubArray(InvokeMethodWithReceiver instruction) {
+    @Override
+    public boolean isAppendWithSubArray(Instruction instruction) {
+      if (!instruction.isInvokeMethodWithReceiver()) {
+        return false;
+      }
       DexMethod invokedMethod = instruction.asInvokeMethod().getInvokedMethod();
       return factory.stringBuilderMethods.isAppendSubArrayMethod(invokedMethod)
           || factory.stringBufferMethods.isAppendSubArrayMethod(invokedMethod);
diff --git a/src/test/examplesJava17/string/StringBuilderWithAppendOutOfBoundsTest.java b/src/test/examplesJava17/string/StringBuilderWithAppendOutOfBoundsTest.java
new file mode 100644
index 0000000..cb3e379
--- /dev/null
+++ b/src/test/examplesJava17/string/StringBuilderWithAppendOutOfBoundsTest.java
@@ -0,0 +1,66 @@
+// Copyright (c) 2024, 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 string;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.utils.StringUtils;
+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 StringBuilderWithAppendOutOfBoundsTest extends TestBase {
+
+  private static final String EXPECTED_OUTPUT =
+      StringUtils.lines("class java.lang.IndexOutOfBoundsException");
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    testForRuntime(parameters)
+        .addInnerClassesAndStrippedOuter(getClass())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClassesAndStrippedOuter(getClass())
+        .addKeepMainRule(Main.class)
+        .addLibraryFiles(ToolHelper.getAndroidJar(35))
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  static class Main {
+
+    public static void main(String[] strArr) {
+      String text = "sss";
+      int l = 7;
+
+      StringBuilder sb = new StringBuilder();
+      try {
+        sb.append(text, 0, l);
+        System.out.println("not out of bounds");
+      } catch (Exception e) {
+        System.out.println(e.getClass());
+      }
+    }
+  }
+}