Replace usages of String.split

Bug: b/270510095
Change-Id: I7f890f8c32968c783067cfdc4369301fc9c65e0a
diff --git a/build.gradle b/build.gradle
index fee1ce4..b771939 100644
--- a/build.gradle
+++ b/build.gradle
@@ -700,14 +700,13 @@
         options.errorprone.check('MultipleTopLevelClasses', CheckSeverity.ERROR)
         options.errorprone.check('NarrowingCompoundAssignment', CheckSeverity.ERROR)
 
-        // TODO(b/270534077): These should likely be fixed/suppressed and become hard failures.
+        // TODO(b/270510095): These should likely be fixed/suppressed and become hard failures.
         options.errorprone.check('UnusedVariable', CheckSeverity.OFF)
         options.errorprone.check('EqualsUnsafeCast', CheckSeverity.OFF)
         options.errorprone.check('TypeParameterUnusedInFormals', CheckSeverity.OFF)
         options.errorprone.check('LoopOverCharArray', CheckSeverity.OFF)
         options.errorprone.check('ImmutableEnumChecker', CheckSeverity.OFF)
         options.errorprone.check('BadImport', CheckSeverity.OFF)
-        options.errorprone.check('StringSplitter', CheckSeverity.OFF)
         options.errorprone.check('ComplexBooleanConstant', CheckSeverity.OFF)
         options.errorprone.check('StreamToIterable', CheckSeverity.OFF)
         options.errorprone.check('HidingField', CheckSeverity.OFF)
diff --git a/d8_r8/main/build.gradle.kts b/d8_r8/main/build.gradle.kts
index fda414f..8334a71 100644
--- a/d8_r8/main/build.gradle.kts
+++ b/d8_r8/main/build.gradle.kts
@@ -47,14 +47,13 @@
   options.errorprone.error("MultipleTopLevelClasses")
   options.errorprone.error("NarrowingCompoundAssignment")
 
-  // TODO(b/270534077): These should likely be fixed/suppressed and become hard failures.
+  // TODO(b/270510095): These should likely be fixed/suppressed and become hard failures.
   options.errorprone.disable("UnusedVariable")
   options.errorprone.disable("EqualsUnsafeCast")
   options.errorprone.disable("TypeParameterUnusedInFormals")
   options.errorprone.disable("LoopOverCharArray")
   options.errorprone.disable("ImmutableEnumChecker")
   options.errorprone.disable("BadImport")
-  options.errorprone.disable("StringSplitter")
   options.errorprone.disable("ComplexBooleanConstant")
   options.errorprone.disable("StreamToIterable")
   options.errorprone.disable("HidingField")
diff --git a/src/main/java/com/android/tools/r8/dump/DumpOptions.java b/src/main/java/com/android/tools/r8/dump/DumpOptions.java
index 0c538fa..6d5e880 100644
--- a/src/main/java/com/android/tools/r8/dump/DumpOptions.java
+++ b/src/main/java/com/android/tools/r8/dump/DumpOptions.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.shaking.ProguardConfigurationRule;
 import com.android.tools.r8.startup.StartupProfileProvider;
 import com.android.tools.r8.utils.InternalOptions.DesugarState;
+import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.ThreadUtils;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -179,17 +180,19 @@
   }
 
   public static void parse(String content, DumpOptions.Builder builder) {
-    String[] lines = content.split("\n");
-    for (String line : lines) {
-      String trimmed = line.trim();
-      int i = trimmed.indexOf('=');
-      if (i < 0) {
-        throw new RuntimeException("Invalid dump line. Expected = in line: '" + trimmed + "'");
-      }
-      String key = trimmed.substring(0, i).trim();
-      String value = trimmed.substring(i + 1).trim();
-      parseKeyValue(builder, key, value);
-    }
+    StringUtils.splitForEach(
+        content,
+        '\n',
+        line -> {
+          String trimmed = line.trim();
+          int i = trimmed.indexOf('=');
+          if (i < 0) {
+            throw new RuntimeException("Invalid dump line. Expected = in line: '" + trimmed + "'");
+          }
+          String key = trimmed.substring(0, i).trim();
+          String value = trimmed.substring(i + 1).trim();
+          parseKeyValue(builder, key, value);
+        });
   }
 
   private static void parseKeyValue(Builder builder, String key, String value) {
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/GenerateHtmlDoc.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/GenerateHtmlDoc.java
index d502339..45aa53f 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/GenerateHtmlDoc.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/GenerateHtmlDoc.java
@@ -17,6 +17,8 @@
 import com.android.tools.r8.ir.desugar.desugaredlibrary.lint.SupportedClasses.MethodAnnotation;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.lint.SupportedClasses.SupportedClass;
 import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.StringUtils;
 import java.io.PrintStream;
 import java.nio.file.Files;
 import java.util.ArrayList;
@@ -124,10 +126,10 @@
       if (rewritten != null) {
         return rewritten;
       }
-      String[] split = packageName.split("\\.");
-      if (split.length > 2) {
-        String prevPackage =
-            packageName.substring(0, packageName.length() - split[split.length - 1].length() - 1);
+      List<String> split = StringUtils.split(packageName, '.');
+      if (split.size() > 2) {
+        String last = ListUtils.last(split);
+        String prevPackage = packageName.substring(0, packageName.length() - last.length() - 1);
         return typeInPackage(typeName, prevPackage);
       }
       return null;
@@ -303,14 +305,14 @@
     }
 
     private String format(String s, int i) {
-      String[] regexpSplit = s.split("\\.");
-      if (regexpSplit.length < i) {
+      List<String> split = StringUtils.split(s, '.');
+      if (split.size() < i) {
         return s;
       }
       int splitIndex = 0;
       int mid = i / 2;
       for (int j = 0; j < mid; j++) {
-        splitIndex += regexpSplit[j].length();
+        splitIndex += split.get(j).length();
       }
       splitIndex += mid;
       return s.substring(0, splitIndex) + HTML_SPLIT + s.substring(splitIndex);
@@ -340,17 +342,17 @@
         return appendLiCode(s);
       }
       StringBuilder sb = new StringBuilder();
-      String[] split = s.split("\\(");
-      sb.append(split[0]).append('(').append(HTML_SPLIT);
-      if (split[1].length() < MAX_LINE_CHARACTERS - 2) {
-        sb.append(split[1]);
+      List<String> split = StringUtils.split(s, '(');
+      sb.append(split.get(0)).append('(').append(HTML_SPLIT);
+      if (split.get(1).length() < MAX_LINE_CHARACTERS - 2) {
+        sb.append(split.get(1));
         return appendLiCode(sb.toString());
       }
-      String[] secondSplit = split[1].split(",");
+      List<String> secondSplit = StringUtils.split(split.get(1), ',');
       sb.append("&nbsp;");
-      for (int i = 0; i < secondSplit.length; i++) {
-        sb.append(secondSplit[i]);
-        if (i != secondSplit.length - 1) {
+      for (int i = 0; i < secondSplit.size(); i++) {
+        sb.append(secondSplit.get(i));
+        if (i != secondSplit.size() - 1) {
           sb.append(',');
           sb.append(HTML_SPLIT);
         }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineDesugaredLibrarySpecification.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineDesugaredLibrarySpecification.java
index c357c2a..31baed9 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineDesugaredLibrarySpecification.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineDesugaredLibrarySpecification.java
@@ -14,7 +14,9 @@
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibrarySpecification;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.specificationconversion.LibraryValidator;
 import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.SemanticVersion;
+import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.Timing;
 import java.util.List;
 import java.util.Map;
@@ -251,8 +253,9 @@
     if (leadingVersionNumberCache != -1) {
       return leadingVersionNumberCache;
     }
-    String[] split = topLevelFlags.getIdentifier().split(":");
-    return leadingVersionNumberCache = SemanticVersion.parse(split[split.length - 1]).getMajor();
+    List<String> split = StringUtils.split(topLevelFlags.getIdentifier(), ':');
+    String last = ListUtils.last(split);
+    return leadingVersionNumberCache = SemanticVersion.parse(last).getMajor();
   }
 
   public boolean includesJDK11Methods() {
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinSourceDebugExtensionParser.java b/src/main/java/com/android/tools/r8/kotlin/KotlinSourceDebugExtensionParser.java
index 065ff92..6cf38cb 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinSourceDebugExtensionParser.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinSourceDebugExtensionParser.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.naming.Range;
 import com.android.tools.r8.utils.SegmentTree;
+import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.ThrowingConsumer;
 import java.io.BufferedReader;
 import java.io.Closeable;
@@ -216,8 +217,8 @@
       throws KotlinSourceDebugExtensionParserException {
     // + <file_number_i> <file_name_i>
     // <file_path_i>
-    String[] entries = entryLine.trim().split(" ");
-    if (entries.length != 3 || !entries[0].equals("+")) {
+    String[] entries = StringUtils.splitKnownSize(entryLine.trim(), ' ', 3);
+    if (entries == null || !entries[0].equals("+")) {
       throw new KotlinSourceDebugExtensionParserException(
           "Wrong number of entries on line " + entryLine);
     }
diff --git a/src/main/java/com/android/tools/r8/naming/IdentifierNameStringUtils.java b/src/main/java/com/android/tools/r8/naming/IdentifierNameStringUtils.java
index 6e14907..3b022c7 100644
--- a/src/main/java/com/android/tools/r8/naming/IdentifierNameStringUtils.java
+++ b/src/main/java/com/android/tools/r8/naming/IdentifierNameStringUtils.java
@@ -34,6 +34,7 @@
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.naming.identifiernamestring.IdentifierNameStringLookupResult;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.List;
@@ -340,15 +341,15 @@
     String identifier = dexString.toString();
     String typeIdentifier = null;
     String memberIdentifier = null;
-    String[] items = identifier.split("#");
+    List<String> items = StringUtils.split(identifier, '#');
     // "x#y#z"
-    if (items.length > 2) {
+    if (items.size() > 2) {
       return null;
     }
     // "fully.qualified.ClassName#fieldOrMethodName"
-    if (items.length == 2) {
-      typeIdentifier = items[0];
-      memberIdentifier = items[1];
+    if (items.size() == 2) {
+      typeIdentifier = items.get(0);
+      memberIdentifier = items.get(1);
     } else {
       int lastDot = identifier.lastIndexOf(".");
       // "fully.qualified.ClassName.fieldOrMethodName"
diff --git a/src/main/java/com/android/tools/r8/utils/DeterminismChecker.java b/src/main/java/com/android/tools/r8/utils/DeterminismChecker.java
index 2f95428..c1817a7 100644
--- a/src/main/java/com/android/tools/r8/utils/DeterminismChecker.java
+++ b/src/main/java/com/android/tools/r8/utils/DeterminismChecker.java
@@ -121,7 +121,7 @@
       return;
     }
     if (method.hasCode()) {
-      String[] lines = method.getCode().toString().split("\n");
+      List<String> lines = StringUtils.splitLines(method.getCode().toString());
       for (String line : lines) {
         if (!callback.onLine(line)) {
           return;
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index b26527a..d3f4b4c 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -938,9 +938,7 @@
     String property = System.getProperty("com.android.tools.r8.extensiveLoggingFilter");
     if (property != null) {
       ImmutableSet.Builder<String> builder = ImmutableSet.builder();
-      for (String method : property.split(";")) {
-        builder.add(method);
-      }
+      StringUtils.splitForEach(property, ';', builder::add);
       return builder.build();
     }
     return ImmutableSet.of();
@@ -951,9 +949,7 @@
         System.getProperty("com.android.tools.r8.extensiveInterfaceMethodMinifierLoggingFilter");
     if (property != null) {
       ImmutableSet.Builder<String> builder = ImmutableSet.builder();
-      for (String method : property.split(";")) {
-        builder.add(method);
-      }
+      StringUtils.splitForEach(property, ';', builder::add);
       return builder.build();
     }
     return ImmutableSet.of();
diff --git a/src/main/java/com/android/tools/r8/utils/StringUtils.java b/src/main/java/com/android/tools/r8/utils/StringUtils.java
index efe43d3..3ba639e 100644
--- a/src/main/java/com/android/tools/r8/utils/StringUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/StringUtils.java
@@ -14,6 +14,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -65,6 +66,68 @@
     return builder.toString();
   }
 
+  // Utilities for splitting (and avoiding errorprone String.split).
+
+  /**
+   * Iterate over the substrings of a string split by a single char separator.
+   *
+   * <p>No special treatment of whitespace. No occurrence of the separator will appear in any
+   * split-off substring. Given N occurrences of the separator, the resulting callback will be
+   * called N+1 times.
+   */
+  public static void splitForEach(String string, char separator, Consumer<String> fn) {
+    int length = string.length();
+    int start = 0;
+    for (int i = 0; i < length; i++) {
+      char c = string.charAt(i);
+      if (c == separator) {
+        fn.accept(string.substring(start, i));
+        start = i + 1;
+      }
+    }
+    fn.accept(string.substring(start));
+  }
+
+  /**
+   * Split a string by a single char separator.
+   *
+   * <p>No special treatment of whitespace. No occurrence of the separator will appear in any
+   * split-off substring. Given N occurrences of the separator, the resulting split list will have
+   * size N+1.
+   */
+  public static List<String> split(String string, char separator) {
+    List<String> result = new ArrayList<>();
+    splitForEach(string, separator, result::add);
+    return result;
+  }
+
+  /**
+   * Split a string by a single char separator with the requirement on the split size.
+   *
+   * <p>No special treatment of whitespace. No occurrence of the separator will appear in any
+   * split-off substring. Given N occurrences of the separator, the resulting split list will have
+   * size N+1.
+   *
+   * <p>Thus for a valid split with size=N, the result will be an array of length N if and only if
+   * the input string has exactly N-1 occurrences of the separator. In any other case the return
+   * value is null.
+   */
+  public static String[] splitKnownSize(String string, char separator, int size) {
+    assert size > 1;
+    String[] result = new String[size];
+    IntBox box = new IntBox(0);
+    splitForEach(
+        string,
+        separator,
+        part -> {
+          int i = box.getAndIncrement();
+          if (i < size) {
+            result[i] = part;
+          }
+        });
+    return size == box.get() ? result : null;
+  }
+
   public static boolean appendNonEmpty(
       StringBuilder builder, String pre, Object item, String post) {
     if (item == null) {
diff --git a/src/test/java/com/android/tools/r8/utils/StringUtilsTest.java b/src/test/java/com/android/tools/r8/utils/StringUtilsTest.java
index acadcc6..2237b07 100644
--- a/src/test/java/com/android/tools/r8/utils/StringUtilsTest.java
+++ b/src/test/java/com/android/tools/r8/utils/StringUtilsTest.java
@@ -3,7 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 
 import com.android.tools.r8.TestBase;
@@ -47,6 +49,18 @@
     assertListEquals(ImmutableList.of("\ra\r\rb\r"), StringUtils.splitLines("\ra\r\rb\r"));
   }
 
+  @Test
+  public void testSplit() {
+    assertListEquals(ImmutableList.of(" "), StringUtils.split(" ", '.'));
+    assertListEquals(ImmutableList.of("", ""), StringUtils.split(".", '.'));
+    assertListEquals(ImmutableList.of("", "", "", ""), StringUtils.split("...", '.'));
+    assertListEquals(ImmutableList.of("a", "b", "c", "d"), StringUtils.split("a.b.c.d", '.'));
+
+    assertArrayEquals(new String[] {"a", "b", "c"}, StringUtils.splitKnownSize("a.b.c", '.', 3));
+    assertNull(StringUtils.splitKnownSize("a.b.c", '.', 2));
+    assertNull(StringUtils.splitKnownSize("a.b.c", '.', 4));
+  }
+
   private void assertListEquals(List<String> xs, List<String> ys) {
     assertEquals(
         StringUtils.join(", ", xs, s -> '"' + StringUtils.toASCIIString(s) + '"', BraceType.SQUARE),