Move string optimizations to library method optimizer

This leads to more optimizations since the existing optimization did only handle symbolic method references where the holder was java.lang.String.

The library method optimizer also works for calls to java.lang.Object methods that dispatch to java.lang.String methods (e.g., java.lang.Object.toString()).

Fixes: b/337995960
Change-Id: Iac1fa9f81ef6389486c847feb190528f28c72538
diff --git a/src/main/java/com/android/tools/r8/dex/ClassesChecksum.java b/src/main/java/com/android/tools/r8/dex/ClassesChecksum.java
index 3ce1c35..8810dd8 100644
--- a/src/main/java/com/android/tools/r8/dex/ClassesChecksum.java
+++ b/src/main/java/com/android/tools/r8/dex/ClassesChecksum.java
@@ -56,7 +56,7 @@
   @SuppressWarnings("EmptyCatch")
   // Try to parse the string as a marker and append its content if successful.
   public void tryParseAndAppend(DexString dexString) {
-    if (dexString.size > 2
+    if (dexString.length() > 2
         && dexString.content[0] == PREFIX_CHAR0
         && dexString.content[1] == PREFIX_CHAR1
         && dexString.content[2] == PREFIX_CHAR2) {
diff --git a/src/main/java/com/android/tools/r8/dex/FileWriter.java b/src/main/java/com/android/tools/r8/dex/FileWriter.java
index 3aa1c44..9a0bcf6 100644
--- a/src/main/java/com/android/tools/r8/dex/FileWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/FileWriter.java
@@ -637,7 +637,7 @@
 
   private void writeStringData(DexString string) {
     mixedSectionOffsets.setOffsetFor(string, dest.position());
-    dest.putUleb128(string.size);
+    dest.putUleb128(string.length());
     dest.putBytes(string.content);
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/DexCallSite.java b/src/main/java/com/android/tools/r8/graph/DexCallSite.java
index 2cf72f1..bf35bfd 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCallSite.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCallSite.java
@@ -219,7 +219,7 @@
     private ObjectOutputStream out;
 
     private void write(DexString string) throws IOException {
-      out.writeInt(string.size); // To avoid same-prefix problem
+      out.writeInt(string.length()); // To avoid same-prefix problem
       out.write(string.content);
     }
 
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 2bb729c..e3eaa24 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -2226,15 +2226,21 @@
         createMethod(stringType, createProto(voidType, stringType), constructorMethodName);
     public final DexMethod contains;
     public final DexMethod startsWith;
+    public final DexMethod substring;
+    public final DexMethod substringWithEndIndex;
     public final DexMethod endsWith;
     public final DexMethod equals;
     public final DexMethod equalsIgnoreCase;
     public final DexMethod contentEqualsCharSequence;
 
     public final DexMethod indexOfInt;
+    public final DexMethod indexOfIntWithFromIndex;
     public final DexMethod indexOfString;
+    public final DexMethod indexOfStringWithFromIndex;
     public final DexMethod lastIndexOfInt;
+    public final DexMethod lastIndexOfIntWithFromIndex;
     public final DexMethod lastIndexOfString;
+    public final DexMethod lastIndexOfStringWithFromIndex;
     public final DexMethod compareTo;
     public final DexMethod compareToIgnoreCase;
 
@@ -2254,38 +2260,46 @@
       length = createMethod(
           stringDescriptor, lengthMethodName, intDescriptor, DexString.EMPTY_ARRAY);
 
-      DexString[] needsOneCharSequence = { charSequenceDescriptor };
-      DexString[] needsOneString = { stringDescriptor };
-      DexString[] needsOneObject = { objectDescriptor };
-      DexString[] needsOneInt = { intDescriptor };
+      DexString[] charSequenceArgs = {charSequenceDescriptor};
+      DexString[] intArgs = {intDescriptor};
+      DexString[] intIntArgs = {intDescriptor, intDescriptor};
+      DexString[] objectArgs = {objectDescriptor};
+      DexString[] stringArgs = {stringDescriptor};
+      DexString[] stringIntArgs = {stringDescriptor, intDescriptor};
 
-      concat = createMethod(stringDescriptor, concatMethodName, stringDescriptor, needsOneString);
-      contains = createMethod(
-          stringDescriptor, containsMethodName, booleanDescriptor, needsOneCharSequence);
-      startsWith = createMethod(
-          stringDescriptor, startsWithMethodName, booleanDescriptor, needsOneString);
-      endsWith = createMethod(
-          stringDescriptor, endsWithMethodName, booleanDescriptor, needsOneString);
-      equals = createMethod(
-          stringDescriptor, equalsMethodName, booleanDescriptor, needsOneObject);
-      equalsIgnoreCase = createMethod(
-          stringDescriptor, equalsIgnoreCaseMethodName, booleanDescriptor, needsOneString);
-      contentEqualsCharSequence = createMethod(
-          stringDescriptor, contentEqualsMethodName, booleanDescriptor, needsOneCharSequence);
+      concat = createMethod(stringDescriptor, concatMethodName, stringDescriptor, stringArgs);
+      contains =
+          createMethod(stringDescriptor, containsMethodName, booleanDescriptor, charSequenceArgs);
+      startsWith =
+          createMethod(stringDescriptor, startsWithMethodName, booleanDescriptor, stringArgs);
+      endsWith = createMethod(stringDescriptor, endsWithMethodName, booleanDescriptor, stringArgs);
+      substring = createMethod(stringDescriptor, substringName, stringDescriptor, intArgs);
+      substringWithEndIndex =
+          createMethod(stringDescriptor, substringName, stringDescriptor, intIntArgs);
+      equals = createMethod(stringDescriptor, equalsMethodName, booleanDescriptor, objectArgs);
+      equalsIgnoreCase =
+          createMethod(stringDescriptor, equalsIgnoreCaseMethodName, booleanDescriptor, stringArgs);
+      contentEqualsCharSequence =
+          createMethod(
+              stringDescriptor, contentEqualsMethodName, booleanDescriptor, charSequenceArgs);
 
-      indexOfString =
-          createMethod(stringDescriptor, indexOfMethodName, intDescriptor, needsOneString);
-      indexOfInt =
-          createMethod(stringDescriptor, indexOfMethodName, intDescriptor, needsOneInt);
+      indexOfString = createMethod(stringDescriptor, indexOfMethodName, intDescriptor, stringArgs);
+      indexOfStringWithFromIndex =
+          createMethod(stringDescriptor, indexOfMethodName, intDescriptor, stringIntArgs);
+      indexOfInt = createMethod(stringDescriptor, indexOfMethodName, intDescriptor, intArgs);
+      indexOfIntWithFromIndex =
+          createMethod(stringDescriptor, indexOfMethodName, intDescriptor, intIntArgs);
       lastIndexOfString =
-          createMethod(stringDescriptor, lastIndexOfMethodName, intDescriptor, needsOneString);
+          createMethod(stringDescriptor, lastIndexOfMethodName, intDescriptor, stringArgs);
+      lastIndexOfStringWithFromIndex =
+          createMethod(stringDescriptor, lastIndexOfMethodName, intDescriptor, stringIntArgs);
       lastIndexOfInt =
-          createMethod(stringDescriptor, lastIndexOfMethodName, intDescriptor, needsOneInt);
-      compareTo =
-          createMethod(stringDescriptor, compareToMethodName, intDescriptor, needsOneString);
+          createMethod(stringDescriptor, lastIndexOfMethodName, intDescriptor, intArgs);
+      lastIndexOfIntWithFromIndex =
+          createMethod(stringDescriptor, lastIndexOfMethodName, intDescriptor, intIntArgs);
+      compareTo = createMethod(stringDescriptor, compareToMethodName, intDescriptor, stringArgs);
       compareToIgnoreCase =
-          createMethod(stringDescriptor, compareToIgnoreCaseMethodName, intDescriptor,
-              needsOneString);
+          createMethod(stringDescriptor, compareToIgnoreCaseMethodName, intDescriptor, stringArgs);
 
       hashCode = createMethod(stringType, createProto(intType), hashCodeMethodName);
       format =
@@ -2301,8 +2315,7 @@
               stringDescriptor,
               new DexString[] {localeDescriptor, stringDescriptor, objectArrayDescriptor});
 
-      valueOf = createMethod(
-          stringDescriptor, valueOfMethodName, stringDescriptor, needsOneObject);
+      valueOf = createMethod(stringDescriptor, valueOfMethodName, stringDescriptor, objectArgs);
       toString = createMethod(
           stringDescriptor, toStringMethodName, stringDescriptor, DexString.EMPTY_ARRAY);
       intern = createMethod(
diff --git a/src/main/java/com/android/tools/r8/graph/DexString.java b/src/main/java/com/android/tools/r8/graph/DexString.java
index 7288292..00a15e8 100644
--- a/src/main/java/com/android/tools/r8/graph/DexString.java
+++ b/src/main/java/com/android/tools/r8/graph/DexString.java
@@ -37,25 +37,49 @@
   public static final DexString[] EMPTY_ARRAY = {};
   private static final int ARRAY_CHARACTER = '[';
 
-  public final int size;  // size of this string, in UTF-16
+  private final int javaLangStringLength; // size of this string, in UTF-16
   public final byte[] content;
 
-  DexString(int size, byte[] content) {
-    this.size = size;
+  DexString(int javaLangStringLength, byte[] content) {
+    this.javaLangStringLength = javaLangStringLength;
     this.content = content;
   }
 
   DexString(String string) {
-    this.size = string.length();
+    this.javaLangStringLength = string.length();
     this.content = encodeToMutf8(string);
   }
 
+  public boolean equalsIgnoreCase(DexString str) {
+    return toString().equalsIgnoreCase(str.toString());
+  }
+
   public char getFirstByteAsChar() {
     return (char) content[0];
   }
 
+  public int indexOf(int ch) {
+    return toString().indexOf(ch);
+  }
+
+  public int indexOf(int ch, int fromIndex) {
+    return toString().indexOf(ch, fromIndex);
+  }
+
+  public int indexOf(DexString str) {
+    return toString().indexOf(str.toString());
+  }
+
+  public int indexOf(DexString str, int fromIndex) {
+    return toString().indexOf(str.toString(), fromIndex);
+  }
+
+  public boolean isEmpty() {
+    return javaLangStringLength == 0;
+  }
+
   public boolean isEqualTo(String string) {
-    if (size != string.length()) {
+    if (javaLangStringLength != string.length()) {
       return false;
     }
     int index = 0;
@@ -72,19 +96,54 @@
       }
       index++;
     }
-    assert index == size;
+    assert index == javaLangStringLength;
     return true;
   }
 
+  public int lastIndexOf(int ch) {
+    return toString().lastIndexOf(ch);
+  }
+
+  public int lastIndexOf(int ch, int fromIndex) {
+    return toString().lastIndexOf(ch, fromIndex);
+  }
+
+  public int lastIndexOf(DexString str) {
+    return toString().lastIndexOf(str.toString());
+  }
+
+  public int lastIndexOf(DexString str, int fromIndex) {
+    return toString().lastIndexOf(str.toString(), fromIndex);
+  }
+
+  public int length() {
+    return javaLangStringLength;
+  }
+
+  public DexString substring(int beginIndex, DexItemFactory factory) {
+    String str = toString();
+    String substring = str.substring(beginIndex);
+    return substring.equals(str) ? this : factory.createString(substring);
+  }
+
+  @SuppressWarnings("InconsistentOverloads")
+  public DexString substring(int beginIndex, int endIndex, DexItemFactory factory) {
+    String str = toString();
+    String substring = str.substring(beginIndex, endIndex);
+    return substring.equals(str) ? this : factory.createString(substring);
+  }
+
+  public DexString trim(DexItemFactory factory) {
+    String str = toString();
+    String trimmed = str.trim();
+    return trimmed.equals(str) ? this : factory.createString(trimmed);
+  }
+
   @Override
   public DexString self() {
     return this;
   }
 
-  public int size() {
-    return size;
-  }
-
   @Override
   public StructuralMapping<DexString> getStructuralMapping() {
     // Structural accept is never accessed as all accept methods are defined directly.
@@ -101,6 +160,18 @@
     return internalCompareTo(other);
   }
 
+  public int javaLangStringCompareTo(DexString other) {
+    return toString().compareTo(other.toString());
+  }
+
+  public int javaLangStringHashCode() {
+    return toString().hashCode();
+  }
+
+  public int compareToIgnoreCase(DexString other) {
+    return toString().compareToIgnoreCase(other.toString());
+  }
+
   @Override
   public int acceptCompareTo(DexString other, CompareToVisitor visitor) {
     return visitor.visitDexString(this, other);
@@ -168,14 +239,14 @@
 
   @Override
   public int computeHashCode() {
-    return size * 7 + Arrays.hashCode(content);
+    return javaLangStringLength * 7 + Arrays.hashCode(content);
   }
 
   @Override
   public boolean computeEquals(Object other) {
     if (other instanceof DexString) {
       DexString o = (DexString) other;
-      return size == o.size && Arrays.equals(content, o.content);
+      return javaLangStringLength == o.javaLangStringLength && Arrays.equals(content, o.content);
     }
     return false;
   }
@@ -198,7 +269,7 @@
   }
 
   private String decode() throws UTFDataFormatException {
-    char[] out = new char[size];
+    char[] out = new char[javaLangStringLength];
     int decodedLength = decodePrefix(out);
     return new String(out, 0, decodedLength);
   }
@@ -244,7 +315,7 @@
   }
 
   public int decodedHashCode() throws UTFDataFormatException {
-    if (size == 0) {
+    if (javaLangStringLength == 0) {
       assert decode().hashCode() == 0;
       return 0;
     }
@@ -516,7 +587,7 @@
   }
 
   public DexString prepend(DexString prefix, DexItemFactory dexItemFactory) {
-    int newSize = prefix.size + this.size;
+    int newSize = prefix.javaLangStringLength + this.javaLangStringLength;
     // Each string ends with a 0 terminating byte, hence the +/- 1.
     byte[] newContent = new byte[prefix.content.length + this.content.length - 1];
     System.arraycopy(prefix.content, 0, newContent, 0, prefix.content.length - 1);
@@ -538,9 +609,9 @@
     // 'L' -> 'Lfoo/bar', for a string 'Lbaz/Qux' the result should be 'Lfoo/bar' + '/' + 'baz/Qux'.
     // 'Lfoo' -> 'L', for a string 'Lfoo/Qux' the result should be 'L' + Qux', thus we remove a '/'.
     boolean insertSeparator =
-        prefix.size == 1 && !rewrittenPrefix.endsWith(factory.descriptorSeparator);
+        prefix.javaLangStringLength == 1 && !rewrittenPrefix.endsWith(factory.descriptorSeparator);
     boolean removeSeparator =
-        rewrittenPrefix.size == 1 && !prefix.endsWith(factory.descriptorSeparator);
+        rewrittenPrefix.javaLangStringLength == 1 && !prefix.endsWith(factory.descriptorSeparator);
     int sizeAdjustment = 0;
     if (insertSeparator) {
       sizeAdjustment += 1;
@@ -549,7 +620,11 @@
     }
     int arrayDim = getArrayDim();
     // In the case that prefix is 'L' we also insert a separator '/' after the destination
-    int newSize = rewrittenPrefix.size + this.size - prefix.size + sizeAdjustment;
+    int newSize =
+        rewrittenPrefix.javaLangStringLength
+            + this.javaLangStringLength
+            - prefix.javaLangStringLength
+            + sizeAdjustment;
     byte[] newContent =
         new byte
             [rewrittenPrefix.content.length
@@ -585,7 +660,7 @@
     }
     byte[] newContent = new byte[content.length - arrayDim];
     System.arraycopy(this.content, arrayDim, newContent, 0, newContent.length);
-    return factory.createString(this.size - arrayDim, newContent);
+    return factory.createString(this.javaLangStringLength - arrayDim, newContent);
   }
 
   private int getArrayDim() {
@@ -600,6 +675,6 @@
     byte[] newContent = new byte[content.length + dimensions];
     Arrays.fill(newContent, 0, dimensions, (byte) '[');
     System.arraycopy(content, 0, newContent, dimensions, content.length);
-    return dexItemFactory.createString(size + dimensions, newContent);
+    return dexItemFactory.createString(javaLangStringLength + dimensions, newContent);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexType.java b/src/main/java/com/android/tools/r8/graph/DexType.java
index 108877b..aa7f88a 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -449,7 +449,7 @@
     }
     DexString newDesc =
         dexItemFactory.createString(
-            descriptor.size - leadingSquareBrackets,
+            descriptor.length() - leadingSquareBrackets,
             Arrays.copyOfRange(
                 descriptor.content, leadingSquareBrackets, descriptor.content.length));
     return dexItemFactory.createType(newDesc);
@@ -464,7 +464,7 @@
     }
     DexString newDesc =
         dexItemFactory.lookupString(
-            descriptor.size - leadingSquareBrackets,
+            descriptor.length() - leadingSquareBrackets,
             Arrays.copyOfRange(
                 descriptor.content, leadingSquareBrackets, descriptor.content.length));
     return dexItemFactory.lookupType(newDesc);
@@ -524,7 +524,7 @@
     assert getArrayTypeDimensions() >= dimension;
     DexString newDesc =
         dexItemFactory.createString(
-            descriptor.size - dimension,
+            descriptor.length() - dimension,
             Arrays.copyOfRange(descriptor.content, dimension, descriptor.content.length));
     return dexItemFactory.createType(newDesc);
   }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalMergeGroup.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalMergeGroup.java
index 9210ef9..138f79a 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalMergeGroup.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalMergeGroup.java
@@ -186,7 +186,7 @@
         break;
       }
       // Select the target with the shortest name.
-      if (current.getType().getDescriptor().size() < target.getType().getDescriptor().size) {
+      if (current.getType().getDescriptor().length() < target.getType().getDescriptor().length()) {
         target = current;
       }
     }
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCode.java b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
index d64ad03..60a7c0c 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCode.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
@@ -5,7 +5,6 @@
 
 import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
 import static com.android.tools.r8.utils.ConsumerUtils.emptyConsumer;
-import static com.google.common.base.Predicates.alwaysFalse;
 
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
@@ -1339,8 +1338,7 @@
           reachablePhis.getSeenSet().forEach(Phi::removeDeadPhi);
           anyPhisRemoved = true;
         } else {
-          anyPhisRemoved |=
-              phi.removeTrivialPhi(builder, affectedValues, emptyConsumer(), alwaysFalse());
+          anyPhisRemoved |= phi.removeTrivialPhi(builder, affectedValues);
         }
       }
     }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Invoke.java b/src/main/java/com/android/tools/r8/ir/code/Invoke.java
index 890516f..4fd3f35 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Invoke.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Invoke.java
@@ -100,6 +100,14 @@
     return getArgument(0);
   }
 
+  public Value getSecondArgument() {
+    return getArgument(1);
+  }
+
+  public Value getThirdArgument() {
+    return getArgument(2);
+  }
+
   public Value getLastArgument() {
     return getArgument(arguments().size() - 1);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Phi.java b/src/main/java/com/android/tools/r8/ir/code/Phi.java
index 341ca0b..a6f39aa 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Phi.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Phi.java
@@ -152,7 +152,7 @@
       builder.constrainType(operand, readConstraint);
       appendOperand(operand);
     }
-    removeTrivialPhi(builder, null, emptyConsumer(), alwaysFalse());
+    removeTrivialPhi(builder);
   }
 
   public void addOperands(List<Value> operands) {
@@ -275,7 +275,15 @@
   }
 
   public void removeTrivialPhi() {
-    removeTrivialPhi(null, null, emptyConsumer(), alwaysFalse());
+    removeTrivialPhi(null);
+  }
+
+  public boolean removeTrivialPhi(IRBuilder builder) {
+    return removeTrivialPhi(builder, null);
+  }
+
+  public boolean removeTrivialPhi(IRBuilder builder, AffectedValues affectedValues) {
+    return removeTrivialPhi(builder, affectedValues, emptyConsumer(), alwaysFalse());
   }
 
   @SuppressWarnings("ReferenceEquality")
diff --git a/src/main/java/com/android/tools/r8/ir/code/Value.java b/src/main/java/com/android/tools/r8/ir/code/Value.java
index d8d3bf8..409c9ca 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Value.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Value.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
@@ -839,6 +840,22 @@
     return definition.getOutConstantConstInstruction();
   }
 
+  public boolean isConstInt() {
+    assert type.isInt();
+    return !hasLocalInfo() && isDefinedByInstructionSatisfying(Instruction::isConstNumber);
+  }
+
+  public int getConstInt() {
+    assert isConstInt();
+    return definition.asConstNumber().getIntValue();
+  }
+
+  public DexString getConstStringOrNull() {
+    return hasLocalInfo() || !isDefinedByInstructionSatisfying(Instruction::isConstString)
+        ? null
+        : definition.asConstString().getValue();
+  }
+
   public boolean isConstNumber() {
     return isConstant() && getConstInstruction().isConstNumber();
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index ad479bd..d5da488 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -30,6 +30,7 @@
 import com.android.tools.r8.ir.code.NumberGenerator;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
+import com.android.tools.r8.ir.conversion.passes.ClassGetNameOptimizer;
 import com.android.tools.r8.ir.conversion.passes.CodeRewriterPassCollection;
 import com.android.tools.r8.ir.conversion.passes.DexConstantOptimizer;
 import com.android.tools.r8.ir.conversion.passes.FilledNewArrayRewriter;
@@ -72,7 +73,6 @@
 import com.android.tools.r8.ir.optimize.membervaluepropagation.R8MemberValuePropagation;
 import com.android.tools.r8.ir.optimize.numberunboxer.NumberUnboxer;
 import com.android.tools.r8.ir.optimize.outliner.Outliner;
-import com.android.tools.r8.ir.optimize.string.StringOptimizer;
 import com.android.tools.r8.lightir.IR2LirConverter;
 import com.android.tools.r8.lightir.Lir2IRConverter;
 import com.android.tools.r8.lightir.LirCode;
@@ -659,7 +659,8 @@
     }
 
     if (!isDebugMode) {
-      new StringOptimizer(appView).run(code, methodProcessor, methodProcessingContext, timing);
+      new ClassGetNameOptimizer(appView)
+          .run(code, methodProcessor, methodProcessingContext, timing);
       timing.begin("Optimize library methods");
       appView
           .libraryMethodOptimizer()
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/ClassGetNameOptimizer.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/ClassGetNameOptimizer.java
new file mode 100644
index 0000000..73f5f8a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/ClassGetNameOptimizer.java
@@ -0,0 +1,268 @@
+// 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 com.android.tools.r8.ir.conversion.passes;
+
+import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
+import static com.android.tools.r8.naming.dexitembasedstring.ClassNameComputationInfo.ClassNameMapping.CANONICAL_NAME;
+import static com.android.tools.r8.naming.dexitembasedstring.ClassNameComputationInfo.ClassNameMapping.NAME;
+import static com.android.tools.r8.naming.dexitembasedstring.ClassNameComputationInfo.ClassNameMapping.SIMPLE_NAME;
+import static com.android.tools.r8.utils.DescriptorUtils.INNER_CLASS_SEPARATOR;
+
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.analysis.escape.EscapeAnalysis;
+import com.android.tools.r8.ir.analysis.escape.EscapeAnalysisConfiguration;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.code.ConstClass;
+import com.android.tools.r8.ir.code.ConstNumber;
+import com.android.tools.r8.ir.code.ConstString;
+import com.android.tools.r8.ir.code.DexItemBasedConstString;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeVirtual;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
+import com.android.tools.r8.ir.optimize.AffectedValues;
+import com.android.tools.r8.naming.dexitembasedstring.ClassNameComputationInfo;
+import com.android.tools.r8.shaking.KeepClassInfo;
+
+public class ClassGetNameOptimizer extends CodeRewriterPass<AppInfo> {
+
+  public ClassGetNameOptimizer(AppView<?> appView) {
+    super(appView);
+  }
+
+  @Override
+  protected String getRewriterId() {
+    return "StringOptimizer";
+  }
+
+  @Override
+  protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
+    if (options.enableNameReflectionOptimization
+        || options.testing.forceNameReflectionOptimization) {
+      return !isDebugMode(code.context());
+    }
+    return false;
+  }
+
+  @Override
+  protected CodeRewriterResult rewriteCode(IRCode code) {
+    boolean hasChanged = rewriteClassGetName(code);
+    return CodeRewriterResult.hasChanged(hasChanged);
+  }
+
+  // Find Class#get*Name() with a constant-class and replace it with a const-string if possible.
+  private boolean rewriteClassGetName(IRCode code) {
+    boolean hasChanged = false;
+    AffectedValues affectedValues = new AffectedValues();
+    InstructionListIterator it = code.instructionListIterator();
+    while (it.hasNext()) {
+      Instruction instr = it.next();
+      if (!instr.isInvokeVirtual()) {
+        continue;
+      }
+      InvokeVirtual invoke = instr.asInvokeVirtual();
+      DexMethod invokedMethod = invoke.getInvokedMethod();
+      if (!dexItemFactory.classMethods.isReflectiveNameLookup(invokedMethod)) {
+        continue;
+      }
+
+      Value out = invoke.outValue();
+      // Skip the call if the computed name is already discarded or not used anywhere.
+      if (out == null || !out.hasAnyUsers()) {
+        continue;
+      }
+
+      assert invoke.inValues().size() == 1;
+      // In case of handling multiple invocations over the same const-string, all the following
+      // usages after the initial one will point to non-null IR (a.k.a. alias), e.g.,
+      //
+      //   rcv <- invoke-virtual instance, ...#getClass() // Can be rewritten to const-class
+      //   x <- invoke-virtual rcv, Class#getName()
+      //   non_null_rcv <- non-null rcv
+      //   y <- invoke-virtual non_null_rcv, Class#getCanonicalName()
+      //   z <- invoke-virtual non_null_rcv, Class#getSimpleName()
+      //   ... // or some other usages of the same usage.
+      //
+      // In that case, we should check if the original source is (possibly rewritten) const-class.
+      Value in = invoke.getReceiver().getAliasedValue();
+      if (in.definition == null || !in.definition.isConstClass() || in.hasLocalInfo()) {
+        continue;
+      }
+
+      ConstClass constClass = in.definition.asConstClass();
+      DexType type = constClass.getValue();
+      int arrayDepth = type.getNumberOfLeadingSquareBrackets();
+      DexType baseType = type.toBaseType(dexItemFactory);
+      // Make sure base type is a class type.
+      if (!baseType.isClassType()) {
+        continue;
+      }
+      DexClass holder = appView.definitionFor(baseType);
+      if (holder == null) {
+        continue;
+      }
+      boolean mayBeRenamed =
+          holder.isProgramClass()
+              && appView
+                  .getKeepInfoOrDefault(holder.asProgramClass(), KeepClassInfo.top())
+                  .isMinificationAllowed(options);
+      // b/120138731: Filter out escaping uses. In such case, the result of this optimization will
+      // be stored somewhere, which can lead to a regression if the corresponding class is in a deep
+      // package hierarchy. For local cases, it is likely a one-time computation, but make sure the
+      // result is used reasonably, such as library calls. For example, if a class may be minified
+      // while its name is used to compute hash code, which won't be optimized, it's better not to
+      // compute the name.
+      if (!appView.options().testing.forceNameReflectionOptimization) {
+        if (mayBeRenamed) {
+          continue;
+        }
+        if (invokedMethod.isNotIdenticalTo(dexItemFactory.classMethods.getSimpleName)) {
+          EscapeAnalysis escapeAnalysis =
+              new EscapeAnalysis(appView, StringOptimizerEscapeAnalysisConfiguration.getInstance());
+          if (escapeAnalysis.isEscaping(code, out)) {
+            continue;
+          }
+        }
+      }
+
+      String descriptor = baseType.toDescriptorString();
+      boolean assumeTopLevel = descriptor.indexOf(INNER_CLASS_SEPARATOR) < 0;
+      DexItemBasedConstString deferred = null;
+      DexString name = null;
+      if (invokedMethod.isIdenticalTo(dexItemFactory.classMethods.getName)) {
+        if (mayBeRenamed) {
+          Value stringValue =
+              code.createValue(
+                  TypeElement.stringClassType(appView, definitelyNotNull()), invoke.getLocalInfo());
+          deferred =
+              new DexItemBasedConstString(
+                  stringValue, baseType, ClassNameComputationInfo.create(NAME, arrayDepth));
+        } else {
+          name = NAME.map(descriptor, holder, dexItemFactory, arrayDepth);
+        }
+      } else if (invokedMethod.isIdenticalTo(dexItemFactory.classMethods.getTypeName)) {
+        // TODO(b/119426668): desugar Type#getTypeName
+        continue;
+      } else if (invokedMethod.isIdenticalTo(dexItemFactory.classMethods.getCanonicalName)) {
+        // Always returns null if the target type is local or anonymous class.
+        if (holder.isLocalClass() || holder.isAnonymousClass()) {
+          ConstNumber constNull = code.createConstNull();
+          it.replaceCurrentInstruction(constNull, affectedValues);
+        } else {
+          // b/119471127: If an outer class is shrunk, we may compute a wrong canonical name.
+          // Leave it as-is so that the class's canonical name is consistent across the app.
+          if (!assumeTopLevel) {
+            continue;
+          }
+          if (mayBeRenamed) {
+            Value stringValue =
+                code.createValue(
+                    TypeElement.stringClassType(appView, definitelyNotNull()),
+                    invoke.getLocalInfo());
+            deferred =
+                new DexItemBasedConstString(
+                    stringValue,
+                    baseType,
+                    ClassNameComputationInfo.create(CANONICAL_NAME, arrayDepth));
+          } else {
+            name = CANONICAL_NAME.map(descriptor, holder, dexItemFactory, arrayDepth);
+          }
+        }
+      } else if (invokedMethod.isIdenticalTo(dexItemFactory.classMethods.getSimpleName)) {
+        // Always returns an empty string if the target type is an anonymous class.
+        if (holder.isAnonymousClass()) {
+          name = dexItemFactory.createString("");
+        } else {
+          // b/120130435: If an outer class is shrunk, we may compute a wrong simple name.
+          // Leave it as-is so that the class's simple name is consistent across the app.
+          if (!assumeTopLevel) {
+            continue;
+          }
+          if (mayBeRenamed) {
+            Value stringValue =
+                code.createValue(
+                    TypeElement.stringClassType(appView, definitelyNotNull()),
+                    invoke.getLocalInfo());
+            deferred =
+                new DexItemBasedConstString(
+                    stringValue,
+                    baseType,
+                    ClassNameComputationInfo.create(SIMPLE_NAME, arrayDepth));
+          } else {
+            name = SIMPLE_NAME.map(descriptor, holder, dexItemFactory, arrayDepth);
+          }
+        }
+      }
+      if (name != null) {
+        Value stringValue =
+            code.createValue(
+                TypeElement.stringClassType(appView, definitelyNotNull()), invoke.getLocalInfo());
+        ConstString constString = new ConstString(stringValue, name);
+        it.replaceCurrentInstruction(constString, affectedValues);
+        hasChanged = true;
+      } else if (deferred != null) {
+        it.replaceCurrentInstruction(deferred, affectedValues);
+        hasChanged = true;
+      }
+    }
+    // Computed name is not null or literally null (for canonical name of local/anonymous class).
+    // In either way, that is narrower information, and thus propagate that.
+    affectedValues.narrowingWithAssumeRemoval(appView, code);
+    return hasChanged;
+  }
+
+  static class StringOptimizerEscapeAnalysisConfiguration implements EscapeAnalysisConfiguration {
+
+    private static final StringOptimizerEscapeAnalysisConfiguration INSTANCE =
+        new StringOptimizerEscapeAnalysisConfiguration();
+
+    private StringOptimizerEscapeAnalysisConfiguration() {}
+
+    public static StringOptimizerEscapeAnalysisConfiguration getInstance() {
+      return INSTANCE;
+    }
+
+    @Override
+    @SuppressWarnings("ReferenceEquality")
+    public boolean isLegitimateEscapeRoute(
+        AppView<?> appView,
+        EscapeAnalysis escapeAnalysis,
+        Instruction escapeRoute,
+        ProgramMethod context) {
+      if (escapeRoute.isReturn() || escapeRoute.isThrow() || escapeRoute.isStaticPut()) {
+        return false;
+      }
+      if (escapeRoute.isInvokeMethod()) {
+        DexMethod invokedMethod = escapeRoute.asInvokeMethod().getInvokedMethod();
+        // b/120138731: Only allow known simple operations on const-string
+        if (invokedMethod.isIdenticalTo(appView.dexItemFactory().stringMembers.hashCode)
+            || invokedMethod.isIdenticalTo(appView.dexItemFactory().stringMembers.isEmpty)
+            || invokedMethod.isIdenticalTo(appView.dexItemFactory().stringMembers.length)) {
+          return true;
+        }
+        // Add more cases to filter out, if any.
+        return false;
+      }
+      if (escapeRoute.isArrayPut()) {
+        Value array = escapeRoute.asArrayPut().array().getAliasedValue();
+        return !array.isPhi() && array.definition.isCreatingArray();
+      }
+      if (escapeRoute.isInstancePut()) {
+        Value instance = escapeRoute.asInstancePut().object().getAliasedValue();
+        return !instance.isPhi() && instance.definition.isNewInstance();
+      }
+      // All other cases are not legitimate.
+      return false;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/AssertionsRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/AssertionsRewriter.java
index 1454841..d002613 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/AssertionsRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/AssertionsRewriter.java
@@ -157,7 +157,7 @@
           result = entry;
           break;
         case PACKAGE:
-          if (entry.value.size == 0) {
+          if (entry.value.length() == 0) {
             if (!type.descriptor.contains(dexItemFactory.descriptorSeparator)) {
               result = entry;
             }
@@ -191,7 +191,7 @@
 
     // Check for inner class name by checking if the prefix is the class descriptor,
     // where ';' is replaced whit '$' and no '/' after that.
-    if (classOrInnerClassDescriptor.size < classDescriptor.size) {
+    if (classOrInnerClassDescriptor.length() < classDescriptor.length()) {
       return false;
     }
     ThrowingCharIterator<UTFDataFormatException> i1 = classDescriptor.iterator();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
index 537b313..23ae714 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
@@ -5,10 +5,8 @@
 package com.android.tools.r8.ir.optimize;
 
 import static com.android.tools.r8.graph.ProgramField.asProgramFieldOrNull;
-import static com.android.tools.r8.utils.ConsumerUtils.emptyConsumer;
 import static com.android.tools.r8.utils.MapUtils.ignoreKey;
 import static com.android.tools.r8.utils.PredicateUtils.not;
-import static com.google.common.base.Predicates.alwaysFalse;
 
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfo;
@@ -482,8 +480,7 @@
               affectedPhis.add(value.asPhi());
             }
           });
-      affectedPhis.forEach(
-          phi -> phi.removeTrivialPhi(null, affectedValues, emptyConsumer(), alwaysFalse()));
+      affectedPhis.forEach(phi -> phi.removeTrivialPhi(null, affectedValues));
       affectedValues.narrowingWithAssumeRemoval(appView, code);
       if (hasChanged) {
         code.removeRedundantBlocks();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
index 48e501a..4918f7c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
@@ -25,7 +25,6 @@
 import com.android.tools.r8.ir.optimize.classinliner.InlineCandidateProcessor.IllegalClassInlinerStateException;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
 import com.android.tools.r8.ir.optimize.inliner.InliningIRProvider;
-import com.android.tools.r8.ir.optimize.string.StringOptimizer;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.LazyBox;
 import com.android.tools.r8.utils.Timing;
@@ -246,8 +245,9 @@
       new BranchSimplifier(appView)
           .run(code, methodProcessor, methodProcessingContext, Timing.empty());
       // If a method was inlined we may see more trivial computation/conversion of String.
-      new StringOptimizer(appView)
-          .run(code, methodProcessor, methodProcessingContext, Timing.empty());
+      appView
+          .libraryMethodOptimizer()
+          .optimize(code, feedback, methodProcessor, methodProcessingContext);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
index 4734657..7b5b88b 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
@@ -893,7 +893,10 @@
 
   private EnumData buildData(DexProgramClass enumClass, Set<DexField> instanceFields) {
     if (!enumClass.hasStaticFields()) {
-      return new EnumData(ImmutableMap.of(), null, ImmutableMap.of(), ImmutableSet.of(), -1);
+      if (instanceFields.isEmpty()) {
+        return new EnumData(ImmutableMap.of(), null, ImmutableMap.of(), ImmutableSet.of(), -1);
+      }
+      return null;
     }
 
     // This map holds all the accessible fields to their unboxed value, so we can remap the field
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryOptimizationInfoCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryOptimizationInfoCollection.java
index 7f6975b..9a31e29 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryOptimizationInfoCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryOptimizationInfoCollection.java
@@ -46,7 +46,7 @@
   private static ComputedMethodOptimizationInfoCollection
       getComputedMethodOptimizationInfoCollection(AppView<?> appView, DexType type) {
     DexItemFactory dexItemFactory = appView.dexItemFactory();
-    switch (type.getDescriptor().size()) {
+    switch (type.getDescriptor().length()) {
       case 16:
         // java.lang.Byte
         // java.lang.Long
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 9b95fb8..dee6c67 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
@@ -42,6 +42,9 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
+import java.util.function.BiPredicate;
+import java.util.function.Predicate;
+import java.util.function.UnaryOperator;
 
 public class StringMethodOptimizer extends StatelessLibraryMethodModelCollection {
   private static boolean DEBUG =
@@ -92,7 +95,6 @@
   }
 
   @Override
-  @SuppressWarnings("ReferenceEquality")
   public InstructionListIterator optimize(
       IRCode code,
       BasicBlockIterator blockIterator,
@@ -103,21 +105,126 @@
       Set<BasicBlock> blocksToRemove) {
     DexMethod singleTargetReference = singleTarget.getReference();
     var stringMembers = dexItemFactory.stringMembers;
-    if (singleTargetReference == stringMembers.equals) {
-      optimizeEquals(code, instructionIterator, invoke.asInvokeMethodWithReceiver());
-    } else if (singleTargetReference == stringMembers.valueOf) {
-      optimizeValueOf(code, instructionIterator, invoke.asInvokeStatic(), affectedValues);
-    } else if (enableStringFormatOptimizations
-        && (singleTargetReference == stringMembers.format
-            || singleTargetReference == stringMembers.formatWithLocale)) {
-      instructionIterator =
-          optimizeFormat(
-              code, instructionIterator, blockIterator, invoke.asInvokeStatic(), affectedValues);
+    switch (singleTarget.getName().getFirstByteAsChar()) {
+      case 'c':
+        if (singleTargetReference.isIdenticalTo(stringMembers.compareTo)) {
+          optimizeStringStringToIntFunction(
+              code, instructionIterator, invoke, DexString::javaLangStringCompareTo);
+        } else if (singleTargetReference.isIdenticalTo(stringMembers.compareToIgnoreCase)) {
+          optimizeStringStringToIntFunction(
+              code, instructionIterator, invoke, DexString::compareToIgnoreCase);
+        } else if (singleTargetReference.isIdenticalTo(stringMembers.contains)) {
+          optimizeStringStringToBooleanFunction(
+              code, instructionIterator, invoke, DexString::contains);
+        } else if (singleTargetReference.isIdenticalTo(stringMembers.contentEqualsCharSequence)) {
+          optimizeStringStringToBooleanFunction(
+              code, instructionIterator, invoke, DexString::isIdenticalTo);
+        }
+        break;
+      case 'e':
+        if (singleTargetReference.isIdenticalTo(stringMembers.endsWith)) {
+          optimizeStringStringToBooleanFunction(
+              code, instructionIterator, invoke, DexString::endsWith);
+        } else if (singleTargetReference.isIdenticalTo(stringMembers.equals)) {
+          if (!optimizeEquals(code, instructionIterator, invoke.asInvokeMethodWithReceiver())) {
+            optimizeStringStringToBooleanFunction(
+                code, instructionIterator, invoke, DexString::isIdenticalTo);
+          }
+        } else if (singleTargetReference.isIdenticalTo(stringMembers.equalsIgnoreCase)) {
+          optimizeStringStringToBooleanFunction(
+              code, instructionIterator, invoke, DexString::equalsIgnoreCase);
+        }
+        break;
+      case 'f':
+        if (singleTargetReference.isIdenticalTo(stringMembers.format)
+            || singleTargetReference.isIdenticalTo(stringMembers.formatWithLocale)) {
+          instructionIterator =
+              optimizeFormat(
+                  code,
+                  instructionIterator,
+                  blockIterator,
+                  invoke.asInvokeStatic(),
+                  affectedValues);
+        }
+        break;
+      case 'h':
+        if (singleTargetReference.isIdenticalTo(stringMembers.hashCode)) {
+          optimizeStringToIntFunction(
+              code, instructionIterator, invoke, DexString::javaLangStringHashCode);
+        }
+        break;
+      case 'i':
+        if (singleTargetReference.isIdenticalTo(stringMembers.indexOfInt)) {
+          optimizeStringIntToIntFunction(code, instructionIterator, invoke, DexString::indexOf);
+        } else if (singleTargetReference.isIdenticalTo(stringMembers.indexOfIntWithFromIndex)) {
+          optimizeStringIntIntToIntFunction(code, instructionIterator, invoke, DexString::indexOf);
+        } else if (singleTargetReference.isIdenticalTo(stringMembers.indexOfString)) {
+          optimizeStringStringToIntFunction(code, instructionIterator, invoke, DexString::indexOf);
+        } else if (singleTargetReference.isIdenticalTo(stringMembers.indexOfStringWithFromIndex)) {
+          optimizeStringStringIntToIntFunction(
+              code, instructionIterator, invoke, DexString::indexOf);
+        } else if (singleTargetReference.isIdenticalTo(stringMembers.isEmpty)) {
+          optimizeStringToBooleanFunction(code, instructionIterator, invoke, DexString::isEmpty);
+        }
+        break;
+      case 'l':
+        if (singleTargetReference.isIdenticalTo(stringMembers.lastIndexOfInt)) {
+          optimizeStringIntToIntFunction(code, instructionIterator, invoke, DexString::lastIndexOf);
+        } else if (singleTargetReference.isIdenticalTo(stringMembers.lastIndexOfIntWithFromIndex)) {
+          optimizeStringIntIntToIntFunction(
+              code, instructionIterator, invoke, DexString::lastIndexOf);
+        } else if (singleTargetReference.isIdenticalTo(stringMembers.lastIndexOfString)) {
+          optimizeStringStringToIntFunction(
+              code, instructionIterator, invoke, DexString::lastIndexOf);
+        } else if (singleTargetReference.isIdenticalTo(
+            stringMembers.lastIndexOfStringWithFromIndex)) {
+          optimizeStringStringIntToIntFunction(
+              code, instructionIterator, invoke, DexString::lastIndexOf);
+        } else if (singleTargetReference.isIdenticalTo(stringMembers.length)) {
+          optimizeStringToIntFunction(code, instructionIterator, invoke, DexString::length);
+        }
+        break;
+      case 's':
+        if (singleTargetReference.isIdenticalTo(stringMembers.startsWith)) {
+          optimizeStringStringToBooleanFunction(
+              code, instructionIterator, invoke, DexString::startsWith);
+        } else if (singleTargetReference.isIdenticalTo(stringMembers.substring)) {
+          optimizeStringIntToStringFunction(
+              code,
+              instructionIterator,
+              invoke,
+              affectedValues,
+              (s, i) -> 0 <= i && i <= s.length() ? s.substring(i, dexItemFactory) : null);
+        } else if (singleTargetReference.isIdenticalTo(stringMembers.substringWithEndIndex)) {
+          optimizeStringIntIntToStringFunction(
+              code,
+              instructionIterator,
+              invoke,
+              affectedValues,
+              (s, i, j) ->
+                  i <= 0 && i <= j && j <= s.length() ? s.substring(i, j, dexItemFactory) : null);
+        }
+        break;
+      case 't':
+        if (singleTargetReference.isIdenticalTo(stringMembers.toString)) {
+          optimizeStringToStringFunction(code, instructionIterator, invoke, affectedValues, s -> s);
+        } else if (singleTargetReference.isIdenticalTo(stringMembers.trim)) {
+          optimizeStringToStringFunction(
+              code, instructionIterator, invoke, affectedValues, str -> str.trim(dexItemFactory));
+        }
+        break;
+      case 'v':
+        if (singleTargetReference.isIdenticalTo(stringMembers.valueOf)) {
+          optimizeValueOf(code, instructionIterator, invoke.asInvokeStatic(), affectedValues);
+        }
+        break;
+      default:
+        break;
     }
     return instructionIterator;
   }
 
-  private void optimizeEquals(
+  private boolean optimizeEquals(
       IRCode code, InstructionListIterator instructionIterator, InvokeMethodWithReceiver invoke) {
     if (appView.appInfo().hasLiveness()) {
       ProgramMethod context = code.context();
@@ -125,9 +232,212 @@
       Value second = invoke.getArgument(1).getAliasedValue();
       if (isPrunedClassNameComparison(first, second, context)
           || isPrunedClassNameComparison(second, first, context)) {
-        instructionIterator.replaceCurrentInstructionWithConstInt(code, 0);
+        instructionIterator.replaceCurrentInstructionWithConstBoolean(code, false);
+        return true;
       }
     }
+    return false;
+  }
+
+  private void optimizeStringToBooleanFunction(
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      Predicate<DexString> fn) {
+    DexString firstArg = invoke.getFirstArgument().getConstStringOrNull();
+    if (firstArg != null) {
+      boolean replacement = fn.test(firstArg);
+      instructionIterator.replaceCurrentInstructionWithConstBoolean(code, replacement);
+    }
+  }
+
+  private interface StringToIntFunction {
+
+    int apply(DexString s);
+  }
+
+  private void optimizeStringToIntFunction(
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      StringToIntFunction fn) {
+    DexString firstArg = invoke.getFirstArgument().getConstStringOrNull();
+    if (firstArg != null) {
+      int replacement = fn.apply(firstArg);
+      instructionIterator.replaceCurrentInstructionWithConstInt(code, replacement);
+    }
+  }
+
+  private void optimizeStringToStringFunction(
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      AffectedValues affectedValues,
+      UnaryOperator<DexString> fn) {
+    DexString firstArg = invoke.getFirstArgument().getConstStringOrNull();
+    if (firstArg != null) {
+      DexString replacement = fn.apply(firstArg);
+      replaceCurrentInstructionWithConstString(
+          code, instructionIterator, invoke, affectedValues, replacement);
+    }
+  }
+
+  private interface StringIntToIntFunction {
+
+    int apply(DexString s, int i);
+  }
+
+  private void optimizeStringIntToIntFunction(
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      StringIntToIntFunction fn) {
+    DexString firstArg = invoke.getFirstArgument().getConstStringOrNull();
+    if (firstArg != null && invoke.getSecondArgument().isConstInt()) {
+      int replacement = fn.apply(firstArg, invoke.getSecondArgument().getConstInt());
+      instructionIterator.replaceCurrentInstructionWithConstInt(code, replacement);
+    }
+  }
+
+  private interface StringIntToStringFunction {
+
+    DexString apply(DexString s, int i);
+  }
+
+  private void optimizeStringIntToStringFunction(
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      AffectedValues affectedValues,
+      StringIntToStringFunction fn) {
+    DexString firstArg = invoke.getFirstArgument().getConstStringOrNull();
+    if (firstArg != null && invoke.getSecondArgument().isConstInt()) {
+      DexString replacement = fn.apply(firstArg, invoke.getSecondArgument().getConstInt());
+      replaceCurrentInstructionWithConstString(
+          code, instructionIterator, invoke, affectedValues, replacement);
+    }
+  }
+
+  private interface StringIntIntToIntFunction {
+
+    int apply(DexString s, int i, int j);
+  }
+
+  private void optimizeStringIntIntToIntFunction(
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      StringIntIntToIntFunction fn) {
+    DexString firstArg = invoke.getFirstArgument().getConstStringOrNull();
+    if (firstArg != null
+        && invoke.getSecondArgument().isConstInt()
+        && invoke.getThirdArgument().isConstInt()) {
+      int replacement =
+          fn.apply(
+              firstArg,
+              invoke.getSecondArgument().getConstInt(),
+              invoke.getThirdArgument().getConstInt());
+      instructionIterator.replaceCurrentInstructionWithConstInt(code, replacement);
+    }
+  }
+
+  private interface StringIntIntToStringFunction {
+
+    DexString apply(DexString s, int i, int j);
+  }
+
+  private void optimizeStringIntIntToStringFunction(
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      AffectedValues affectedValues,
+      StringIntIntToStringFunction fn) {
+    DexString firstArg = invoke.getFirstArgument().getConstStringOrNull();
+    if (firstArg != null
+        && invoke.getSecondArgument().isConstInt()
+        && invoke.getThirdArgument().isConstInt()) {
+      DexString replacement =
+          fn.apply(
+              firstArg,
+              invoke.getSecondArgument().getConstInt(),
+              invoke.getThirdArgument().getConstInt());
+      replaceCurrentInstructionWithConstString(
+          code, instructionIterator, invoke, affectedValues, replacement);
+    }
+  }
+
+  private void optimizeStringStringToBooleanFunction(
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      BiPredicate<DexString, DexString> fn) {
+    DexString firstArg = invoke.getFirstArgument().getConstStringOrNull();
+    DexString secondArg = invoke.getSecondArgument().getConstStringOrNull();
+    if (firstArg != null && secondArg != null) {
+      boolean replacement = fn.test(firstArg, secondArg);
+      instructionIterator.replaceCurrentInstructionWithConstBoolean(code, replacement);
+    }
+  }
+
+  private interface StringStringToIntFunction {
+
+    int apply(DexString s, DexString t);
+  }
+
+  private void optimizeStringStringToIntFunction(
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      StringStringToIntFunction fn) {
+    DexString firstArg = invoke.getFirstArgument().getConstStringOrNull();
+    DexString secondArg = invoke.getSecondArgument().getConstStringOrNull();
+    if (firstArg != null && secondArg != null) {
+      int replacement = fn.apply(firstArg, secondArg);
+      instructionIterator.replaceCurrentInstructionWithConstInt(code, replacement);
+    }
+  }
+
+  private interface StringStringIntToIntFunction {
+
+    int apply(DexString s, DexString t, int i);
+  }
+
+  private void optimizeStringStringIntToIntFunction(
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      StringStringIntToIntFunction fn) {
+    DexString firstArg = invoke.getFirstArgument().getConstStringOrNull();
+    DexString secondArg = invoke.getSecondArgument().getConstStringOrNull();
+    if (firstArg != null && secondArg != null && invoke.getThirdArgument().isConstInt()) {
+      int replacement = fn.apply(firstArg, secondArg, invoke.getThirdArgument().getConstInt());
+      instructionIterator.replaceCurrentInstructionWithConstInt(code, replacement);
+    }
+  }
+
+  private void replaceCurrentInstructionWithConstString(
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      AffectedValues affectedValues,
+      DexString replacement) {
+    assert invoke.getFirstArgument().isConstString();
+    if (replacement == null) {
+      return;
+    }
+    if (replacement.isIdenticalTo(invoke.getFirstArgument().getConstStringOrNull())) {
+      if (invoke.hasOutValue()) {
+        invoke.outValue().replaceUsers(invoke.getFirstArgument(), affectedValues);
+        invoke
+            .getFirstArgument()
+            .uniquePhiUsers()
+            .forEach(phi -> phi.removeTrivialPhi(null, affectedValues));
+      }
+      instructionIterator.removeOrReplaceByDebugLocalRead();
+    } else {
+      instructionIterator.replaceCurrentInstructionWithConstString(
+          appView, code, replacement, affectedValues);
+    }
   }
 
   private static class SimpleStringFormatSpec {
@@ -272,6 +582,9 @@
       BasicBlockIterator blockIterator,
       InvokeStatic formatInvoke,
       AffectedValues affectedValues) {
+    if (!enableStringFormatOptimizations) {
+      return instructionIterator;
+    }
     boolean hasLocale =
         formatInvoke
             .getInvokedMethod()
@@ -456,23 +769,20 @@
       InvokeStatic invoke,
       AffectedValues affectedValues) {
     Value object = invoke.getFirstArgument();
-    TypeElement type = object.getType();
-
-    // Optimize String.valueOf(null) into "null".
-    if (type.isDefinitelyNull()) {
+    if (object.isAlwaysNull(appView)) {
+      // Optimize String.valueOf(null) into "null".
       DexString nullString = dexItemFactory.createString("null");
       instructionIterator.replaceCurrentInstructionWithConstString(
           appView, code, nullString, affectedValues);
-      return;
-    }
-
-    // Optimize String.valueOf(nonNullString) into nonNullString.
-    if (type.isDefinitelyNotNull() && type.isStringType(dexItemFactory)) {
-      if (invoke.hasOutValue()) {
-        affectedValues.addAll(invoke.outValue().affectedValues());
-        invoke.outValue().replaceUsers(object);
+    } else {
+      TypeElement type = object.getType();
+      if (type.isDefinitelyNotNull() && type.isStringType(dexItemFactory)) {
+        // Optimize String.valueOf(nonNullString) into nonNullString.
+        if (invoke.hasOutValue()) {
+          invoke.outValue().replaceUsers(object, affectedValues);
+        }
+        instructionIterator.removeOrReplaceByDebugLocalRead();
       }
-      instructionIterator.removeOrReplaceByDebugLocalRead();
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
deleted file mode 100644
index ad4c24f..0000000
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
+++ /dev/null
@@ -1,533 +0,0 @@
-// Copyright (c) 2018, 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.ir.analysis.type.Nullability.definitelyNotNull;
-import static com.android.tools.r8.ir.optimize.CodeRewriter.removeOrReplaceByDebugLocalWrite;
-import static com.android.tools.r8.naming.dexitembasedstring.ClassNameComputationInfo.ClassNameMapping.CANONICAL_NAME;
-import static com.android.tools.r8.naming.dexitembasedstring.ClassNameComputationInfo.ClassNameMapping.NAME;
-import static com.android.tools.r8.naming.dexitembasedstring.ClassNameComputationInfo.ClassNameMapping.SIMPLE_NAME;
-import static com.android.tools.r8.utils.DescriptorUtils.INNER_CLASS_SEPARATOR;
-
-import com.android.tools.r8.errors.Unreachable;
-import com.android.tools.r8.graph.AppInfo;
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexString;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.analysis.escape.EscapeAnalysis;
-import com.android.tools.r8.ir.analysis.escape.EscapeAnalysisConfiguration;
-import com.android.tools.r8.ir.analysis.type.TypeElement;
-import com.android.tools.r8.ir.code.ConstClass;
-import com.android.tools.r8.ir.code.ConstNumber;
-import com.android.tools.r8.ir.code.ConstString;
-import com.android.tools.r8.ir.code.DexItemBasedConstString;
-import com.android.tools.r8.ir.code.IRCode;
-import com.android.tools.r8.ir.code.Instruction;
-import com.android.tools.r8.ir.code.InstructionListIterator;
-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.ir.conversion.MethodProcessor;
-import com.android.tools.r8.ir.conversion.passes.CodeRewriterPass;
-import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
-import com.android.tools.r8.ir.optimize.AffectedValues;
-import com.android.tools.r8.naming.dexitembasedstring.ClassNameComputationInfo;
-import com.android.tools.r8.shaking.KeepClassInfo;
-import java.io.UTFDataFormatException;
-import java.util.function.BiFunction;
-import java.util.function.Function;
-
-public class StringOptimizer extends CodeRewriterPass<AppInfo> {
-
-  public StringOptimizer(AppView<?> appView) {
-    super(appView);
-  }
-
-  @Override
-  protected String getRewriterId() {
-    return "StringOptimizer";
-  }
-
-  @Override
-  protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
-    return !isDebugMode(code.context());
-  }
-
-  @Override
-  protected CodeRewriterResult rewriteCode(IRCode code) {
-    boolean hasChanged = false;
-    if (options.enableNameReflectionOptimization
-        || options.testing.forceNameReflectionOptimization) {
-      hasChanged |= rewriteClassGetName(code);
-    }
-    if (code.metadata().mayHaveConstString()) {
-      hasChanged |= computeTrivialOperationsOnConstString(code);
-    }
-    hasChanged |= removeTrivialConversions(code);
-    return CodeRewriterResult.hasChanged(hasChanged);
-  }
-
-  @SuppressWarnings("ReferenceEquality")
-  // boolean String#isEmpty()
-  // boolean String#startsWith(String)
-  // boolean String#endsWith(String)
-  // boolean String#contains(String)
-  // boolean String#equals(String)
-  // boolean String#equalsIgnoreCase(String)
-  // boolean String#contentEquals(String)
-  // int String#hashCode()
-  // int String#length()
-  // int String#indexOf(String)
-  // int String#indexOf(int)
-  // int String#lastIndexOf(String)
-  // int String#lastIndexOf(int)
-  // int String#compareTo(String)
-  // int String#compareToIgnoreCase(String)
-  // String String#substring(int)
-  // String String#substring(int, int)
-  // String String#trim()
-  private boolean computeTrivialOperationsOnConstString(IRCode code) {
-    boolean hasChanged = false;
-    AffectedValues affectedValues = new AffectedValues();
-    InstructionListIterator it = code.instructionListIterator();
-    while (it.hasNext()) {
-      Instruction instr = it.next();
-      if (!instr.isInvokeVirtual()) {
-        continue;
-      }
-      InvokeVirtual invoke = instr.asInvokeVirtual();
-      if (!invoke.hasOutValue()) {
-        continue;
-      }
-      DexMethod invokedMethod = invoke.getInvokedMethod();
-      if (invokedMethod.getHolderType() != dexItemFactory.stringType) {
-        continue;
-      }
-      if (invokedMethod.getName() == dexItemFactory.substringName) {
-        assert invoke.inValues().size() == 2 || invoke.inValues().size() == 3;
-        Value rcv = invoke.getReceiver().getAliasedValue();
-        if (rcv.definition == null
-            || !rcv.definition.isConstString()
-            || rcv.hasLocalInfo()) {
-          continue;
-        }
-        Value beginIndex = invoke.inValues().get(1).getAliasedValue();
-        if (beginIndex.definition == null
-            || !beginIndex.definition.isConstNumber()
-            || beginIndex.hasLocalInfo()) {
-          continue;
-        }
-        int beginIndexValue = beginIndex.definition.asConstNumber().getIntValue();
-        Value endIndex = null;
-        if (invoke.inValues().size() == 3) {
-          endIndex = invoke.inValues().get(2).getAliasedValue();
-          if (endIndex.definition == null
-              || !endIndex.definition.isConstNumber()
-              || endIndex.hasLocalInfo()) {
-            continue;
-          }
-        }
-        String rcvString = rcv.definition.asConstString().getValue().toString();
-        int endIndexValue =
-            endIndex == null
-                ? rcvString.length()
-                : endIndex.definition.asConstNumber().getIntValue();
-        if (beginIndexValue < 0
-            || endIndexValue > rcvString.length()
-            || beginIndexValue > endIndexValue) {
-          // This will raise StringIndexOutOfBoundsException.
-          continue;
-        }
-        String sub = rcvString.substring(beginIndexValue, endIndexValue);
-        Value stringValue =
-            code.createValue(
-                TypeElement.stringClassType(appView, definitelyNotNull()), invoke.getLocalInfo());
-        affectedValues.addAll(invoke.outValue().affectedValues());
-        it.replaceCurrentInstruction(
-            new ConstString(stringValue, dexItemFactory.createString(sub)));
-        continue;
-      }
-
-      if (invokedMethod == dexItemFactory.stringMembers.trim) {
-        Value receiver = invoke.getReceiver().getAliasedValue();
-        if (receiver.hasLocalInfo() || receiver.isPhi() || !receiver.definition.isConstString()) {
-          continue;
-        }
-        DexString resultString =
-            dexItemFactory.createString(
-                receiver.definition.asConstString().getValue().toString().trim());
-        Value newOutValue =
-            code.createValue(
-                TypeElement.stringClassType(appView, definitelyNotNull()), invoke.getLocalInfo());
-        it.replaceCurrentInstruction(new ConstString(newOutValue, resultString), affectedValues);
-        continue;
-      }
-
-      Function<DexString, Integer> operatorWithNoArg = null;
-      BiFunction<DexString, DexString, Integer> operatorWithString = null;
-      BiFunction<DexString, Integer, Integer> operatorWithInt = null;
-      if (invokedMethod == dexItemFactory.stringMembers.hashCode) {
-        operatorWithNoArg = rcv -> {
-          try {
-            return rcv.decodedHashCode();
-          } catch (UTFDataFormatException e) {
-            // It is already guaranteed that the string does not throw.
-            throw new Unreachable();
-          }
-        };
-      } else if (invokedMethod == dexItemFactory.stringMembers.length) {
-        operatorWithNoArg = rcv -> rcv.size;
-      } else if (invokedMethod == dexItemFactory.stringMembers.isEmpty) {
-        operatorWithNoArg = rcv -> rcv.size == 0 ? 1 : 0;
-      } else if (invokedMethod == dexItemFactory.stringMembers.contains) {
-        operatorWithString = (rcv, arg) -> rcv.toString().contains(arg.toString()) ? 1 : 0;
-      } else if (invokedMethod == dexItemFactory.stringMembers.startsWith) {
-        operatorWithString = (rcv, arg) -> rcv.startsWith(arg) ? 1 : 0;
-      } else if (invokedMethod == dexItemFactory.stringMembers.endsWith) {
-        operatorWithString = (rcv, arg) -> rcv.endsWith(arg) ? 1 : 0;
-      } else if (invokedMethod == dexItemFactory.stringMembers.equals) {
-        operatorWithString = (rcv, arg) -> rcv.equals(arg) ? 1 : 0;
-      } else if (invokedMethod == dexItemFactory.stringMembers.equalsIgnoreCase) {
-        operatorWithString = (rcv, arg) -> rcv.toString().equalsIgnoreCase(arg.toString()) ? 1 : 0;
-      } else if (invokedMethod == dexItemFactory.stringMembers.contentEqualsCharSequence) {
-        operatorWithString = (rcv, arg) -> rcv.toString().contentEquals(arg.toString()) ? 1 : 0;
-      } else if (invokedMethod == dexItemFactory.stringMembers.indexOfInt) {
-        operatorWithInt = (rcv, idx) -> rcv.toString().indexOf(idx);
-      } else if (invokedMethod == dexItemFactory.stringMembers.indexOfString) {
-        operatorWithString = (rcv, arg) -> rcv.toString().indexOf(arg.toString());
-      } else if (invokedMethod == dexItemFactory.stringMembers.lastIndexOfInt) {
-        operatorWithInt = (rcv, idx) -> rcv.toString().lastIndexOf(idx);
-      } else if (invokedMethod == dexItemFactory.stringMembers.lastIndexOfString) {
-        operatorWithString = (rcv, arg) -> rcv.toString().lastIndexOf(arg.toString());
-      } else if (invokedMethod == dexItemFactory.stringMembers.compareTo) {
-        operatorWithString = (rcv, arg) -> rcv.toString().compareTo(arg.toString());
-      } else if (invokedMethod == dexItemFactory.stringMembers.compareToIgnoreCase) {
-        operatorWithString = (rcv, arg) -> rcv.toString().compareToIgnoreCase(arg.toString());
-      } else {
-        continue;
-      }
-      Value rcv = invoke.getReceiver().getAliasedValue();
-      if (rcv.definition == null
-          || !rcv.definition.isConstString()
-          || rcv.definition.asConstString().instructionInstanceCanThrow(appView, code.context())
-          || rcv.hasLocalInfo()) {
-        continue;
-      }
-      DexString rcvString = rcv.definition.asConstString().getValue();
-
-      ConstNumber constNumber;
-      if (operatorWithNoArg != null) {
-        assert invoke.inValues().size() == 1;
-        int v = operatorWithNoArg.apply(rcvString);
-        constNumber = code.createIntConstant(v);
-      } else if (operatorWithString != null) {
-        assert invoke.inValues().size() == 2;
-        Value arg = invoke.inValues().get(1).getAliasedValue();
-        if (arg.definition == null
-            || !arg.definition.isConstString()
-            || arg.hasLocalInfo()) {
-          continue;
-        }
-        int v = operatorWithString.apply(rcvString, arg.definition.asConstString().getValue());
-        constNumber = code.createIntConstant(v);
-      } else {
-        assert operatorWithInt != null;
-        assert invoke.inValues().size() == 2;
-        Value arg = invoke.inValues().get(1).getAliasedValue();
-        if (arg.definition == null
-            || !arg.definition.isConstNumber()
-            || arg.hasLocalInfo()) {
-          continue;
-        }
-        int v = operatorWithInt.apply(rcvString, arg.definition.asConstNumber().getIntValue());
-        constNumber = code.createIntConstant(v);
-      }
-
-      hasChanged = true;
-      it.replaceCurrentInstruction(constNumber);
-    }
-    // Computed substring is not null, and thus propagate that information.
-    affectedValues.narrowingWithAssumeRemoval(appView, code);
-    return hasChanged;
-  }
-
-  @SuppressWarnings("ReferenceEquality")
-  // Find Class#get*Name() with a constant-class and replace it with a const-string if possible.
-  private boolean rewriteClassGetName(IRCode code) {
-    boolean hasChanged = false;
-    AffectedValues affectedValues = new AffectedValues();
-    InstructionListIterator it = code.instructionListIterator();
-    while (it.hasNext()) {
-      Instruction instr = it.next();
-      if (!instr.isInvokeVirtual()) {
-        continue;
-      }
-      InvokeVirtual invoke = instr.asInvokeVirtual();
-      DexMethod invokedMethod = invoke.getInvokedMethod();
-      if (!dexItemFactory.classMethods.isReflectiveNameLookup(invokedMethod)) {
-        continue;
-      }
-
-      Value out = invoke.outValue();
-      // Skip the call if the computed name is already discarded or not used anywhere.
-      if (out == null || !out.hasAnyUsers()) {
-        continue;
-      }
-
-      assert invoke.inValues().size() == 1;
-      // In case of handling multiple invocations over the same const-string, all the following
-      // usages after the initial one will point to non-null IR (a.k.a. alias), e.g.,
-      //
-      //   rcv <- invoke-virtual instance, ...#getClass() // Can be rewritten to const-class
-      //   x <- invoke-virtual rcv, Class#getName()
-      //   non_null_rcv <- non-null rcv
-      //   y <- invoke-virtual non_null_rcv, Class#getCanonicalName()
-      //   z <- invoke-virtual non_null_rcv, Class#getSimpleName()
-      //   ... // or some other usages of the same usage.
-      //
-      // In that case, we should check if the original source is (possibly rewritten) const-class.
-      Value in = invoke.getReceiver().getAliasedValue();
-      if (in.definition == null
-          || !in.definition.isConstClass()
-          || in.hasLocalInfo()) {
-        continue;
-      }
-
-      ConstClass constClass = in.definition.asConstClass();
-      DexType type = constClass.getValue();
-      int arrayDepth = type.getNumberOfLeadingSquareBrackets();
-      DexType baseType = type.toBaseType(dexItemFactory);
-      // Make sure base type is a class type.
-      if (!baseType.isClassType()) {
-        continue;
-      }
-      DexClass holder = appView.definitionFor(baseType);
-      if (holder == null) {
-        continue;
-      }
-      boolean mayBeRenamed =
-          holder.isProgramClass()
-              && appView
-                  .getKeepInfoOrDefault(holder.asProgramClass(), KeepClassInfo.top())
-                  .isMinificationAllowed(options);
-      // b/120138731: Filter out escaping uses. In such case, the result of this optimization will
-      // be stored somewhere, which can lead to a regression if the corresponding class is in a deep
-      // package hierarchy. For local cases, it is likely a one-time computation, but make sure the
-      // result is used reasonably, such as library calls. For example, if a class may be minified
-      // while its name is used to compute hash code, which won't be optimized, it's better not to
-      // compute the name.
-      if (!appView.options().testing.forceNameReflectionOptimization) {
-        if (mayBeRenamed) {
-          continue;
-        }
-        if (invokedMethod != dexItemFactory.classMethods.getSimpleName) {
-          EscapeAnalysis escapeAnalysis =
-              new EscapeAnalysis(appView, StringOptimizerEscapeAnalysisConfiguration.getInstance());
-          if (escapeAnalysis.isEscaping(code, out)) {
-            continue;
-          }
-        }
-      }
-
-      String descriptor = baseType.toDescriptorString();
-      boolean assumeTopLevel = descriptor.indexOf(INNER_CLASS_SEPARATOR) < 0;
-      DexItemBasedConstString deferred = null;
-      DexString name = null;
-      if (invokedMethod == dexItemFactory.classMethods.getName) {
-        if (mayBeRenamed) {
-          Value stringValue =
-              code.createValue(
-                  TypeElement.stringClassType(appView, definitelyNotNull()), invoke.getLocalInfo());
-          deferred =
-              new DexItemBasedConstString(
-                  stringValue, baseType, ClassNameComputationInfo.create(NAME, arrayDepth));
-        } else {
-          name = NAME.map(descriptor, holder, dexItemFactory, arrayDepth);
-        }
-      } else if (invokedMethod == dexItemFactory.classMethods.getTypeName) {
-        // TODO(b/119426668): desugar Type#getTypeName
-        continue;
-      } else if (invokedMethod == dexItemFactory.classMethods.getCanonicalName) {
-        // Always returns null if the target type is local or anonymous class.
-        if (holder.isLocalClass() || holder.isAnonymousClass()) {
-          ConstNumber constNull = code.createConstNull();
-          it.replaceCurrentInstruction(constNull, affectedValues);
-        } else {
-          // b/119471127: If an outer class is shrunk, we may compute a wrong canonical name.
-          // Leave it as-is so that the class's canonical name is consistent across the app.
-          if (!assumeTopLevel) {
-            continue;
-          }
-          if (mayBeRenamed) {
-            Value stringValue =
-                code.createValue(
-                    TypeElement.stringClassType(appView, definitelyNotNull()),
-                    invoke.getLocalInfo());
-            deferred =
-                new DexItemBasedConstString(
-                    stringValue,
-                    baseType,
-                    ClassNameComputationInfo.create(CANONICAL_NAME, arrayDepth));
-          } else {
-            name = CANONICAL_NAME.map(descriptor, holder, dexItemFactory, arrayDepth);
-          }
-        }
-      } else if (invokedMethod == dexItemFactory.classMethods.getSimpleName) {
-        // Always returns an empty string if the target type is an anonymous class.
-        if (holder.isAnonymousClass()) {
-          name = dexItemFactory.createString("");
-        } else {
-          // b/120130435: If an outer class is shrunk, we may compute a wrong simple name.
-          // Leave it as-is so that the class's simple name is consistent across the app.
-          if (!assumeTopLevel) {
-            continue;
-          }
-          if (mayBeRenamed) {
-            Value stringValue =
-                code.createValue(
-                    TypeElement.stringClassType(appView, definitelyNotNull()),
-                    invoke.getLocalInfo());
-            deferred =
-                new DexItemBasedConstString(
-                    stringValue,
-                    baseType,
-                    ClassNameComputationInfo.create(SIMPLE_NAME, arrayDepth));
-          } else {
-            name = SIMPLE_NAME.map(descriptor, holder, dexItemFactory, arrayDepth);
-          }
-        }
-      }
-      if (name != null) {
-        Value stringValue =
-            code.createValue(
-                TypeElement.stringClassType(appView, definitelyNotNull()), invoke.getLocalInfo());
-        ConstString constString = new ConstString(stringValue, name);
-        it.replaceCurrentInstruction(constString, affectedValues);
-        hasChanged = true;
-      } else if (deferred != null) {
-        it.replaceCurrentInstruction(deferred, affectedValues);
-        hasChanged = true;
-      }
-    }
-    // Computed name is not null or literally null (for canonical name of local/anonymous class).
-    // In either way, that is narrower information, and thus propagate that.
-    affectedValues.narrowingWithAssumeRemoval(appView, code);
-    return hasChanged;
-  }
-
-  @SuppressWarnings("ReferenceEquality")
-  // String#valueOf(null) -> "null"
-  // String#valueOf(String s) -> s
-  // str.toString() -> str
-  private boolean removeTrivialConversions(IRCode code) {
-    boolean hasChanged = false;
-    AffectedValues affectedValues = new AffectedValues();
-    InstructionListIterator it = code.instructionListIterator();
-    while (it.hasNext()) {
-      Instruction instr = it.next();
-      if (instr.isInvokeStatic()) {
-        InvokeStatic invoke = instr.asInvokeStatic();
-        DexMethod invokedMethod = invoke.getInvokedMethod();
-        if (invokedMethod != dexItemFactory.stringMembers.valueOf) {
-          continue;
-        }
-        assert invoke.inValues().size() == 1;
-        Value in = invoke.inValues().get(0);
-        if (in.hasLocalInfo()) {
-          continue;
-        }
-        Value out = invoke.outValue();
-        TypeElement inType = in.getType();
-        if (out != null && in.isAlwaysNull(appView)) {
-          it.replaceCurrentInstructionWithConstString(
-              appView, code, dexItemFactory.createString("null"), affectedValues);
-        } else if (inType.nullability().isDefinitelyNotNull()
-            && inType.isClassType()
-            && inType.asClassType().getClassType().equals(dexItemFactory.stringType)) {
-          if (out != null) {
-            affectedValues.addAll(out.affectedValues());
-            removeOrReplaceByDebugLocalWrite(invoke, it, in, out);
-          } else {
-            it.removeOrReplaceByDebugLocalRead();
-          }
-        }
-        hasChanged = true;
-      } else if (instr.isInvokeVirtual()) {
-        InvokeVirtual invoke = instr.asInvokeVirtual();
-        DexMethod invokedMethod = invoke.getInvokedMethod();
-        if (invokedMethod != dexItemFactory.stringMembers.toString) {
-          continue;
-        }
-        assert invoke.inValues().size() == 1;
-        Value in = invoke.getReceiver();
-        TypeElement inType = in.getType();
-        if (inType.nullability().isDefinitelyNotNull()
-            && inType.isClassType()
-            && inType.asClassType().getClassType().equals(dexItemFactory.stringType)) {
-          Value out = invoke.outValue();
-          if (out != null) {
-            affectedValues.addAll(out.affectedValues());
-            removeOrReplaceByDebugLocalWrite(invoke, it, in, out);
-          } else {
-            it.removeOrReplaceByDebugLocalRead();
-          }
-        }
-        hasChanged = true;
-      }
-    }
-    // Newly added "null" string is not null, and thus propagate that information.
-    affectedValues.narrowingWithAssumeRemoval(appView, code);
-    code.removeRedundantBlocks();
-    return hasChanged;
-  }
-
-  static class StringOptimizerEscapeAnalysisConfiguration
-      implements EscapeAnalysisConfiguration {
-
-    private static final StringOptimizerEscapeAnalysisConfiguration INSTANCE =
-        new StringOptimizerEscapeAnalysisConfiguration();
-
-    private StringOptimizerEscapeAnalysisConfiguration() {}
-
-    public static StringOptimizerEscapeAnalysisConfiguration getInstance() {
-      return INSTANCE;
-    }
-
-    @Override
-    @SuppressWarnings("ReferenceEquality")
-    public boolean isLegitimateEscapeRoute(
-        AppView<?> appView,
-        EscapeAnalysis escapeAnalysis,
-        Instruction escapeRoute,
-        ProgramMethod context) {
-      if (escapeRoute.isReturn() || escapeRoute.isThrow() || escapeRoute.isStaticPut()) {
-        return false;
-      }
-      if (escapeRoute.isInvokeMethod()) {
-        DexMethod invokedMethod = escapeRoute.asInvokeMethod().getInvokedMethod();
-        // b/120138731: Only allow known simple operations on const-string
-        if (invokedMethod == appView.dexItemFactory().stringMembers.hashCode
-            || invokedMethod == appView.dexItemFactory().stringMembers.isEmpty
-            || invokedMethod == appView.dexItemFactory().stringMembers.length) {
-          return true;
-        }
-        // Add more cases to filter out, if any.
-        return false;
-      }
-      if (escapeRoute.isArrayPut()) {
-        Value array = escapeRoute.asArrayPut().array().getAliasedValue();
-        return !array.isPhi() && array.definition.isCreatingArray();
-      }
-      if (escapeRoute.isInstancePut()) {
-        Value instance = escapeRoute.asInstancePut().object().getAliasedValue();
-        return !instance.isPhi() && instance.definition.isNewInstance();
-      }
-      // All other cases are not legitimate.
-      return false;
-    }
-  }
-}