ServiceLoader: Optimize ServiceLoader.iterator().next()

The optimization is enabled only when the following rule is present:
-assumenosideeffects class java.util.ServiceLoader {
  *** load(...);
}

Bug: b/291923475
Change-Id: If73bb7c612a3b6a02c790fa69aced09e3ba694e8
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 a7e37bc..3c056f8 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -249,6 +249,8 @@
 
   public final DexString runtimeExceptionDescriptor = createString("Ljava/lang/RuntimeException;");
   public final DexString assertionErrorDescriptor = createString("Ljava/lang/AssertionError;");
+  public final DexString noSuchElementExceptionDescriptor =
+      createString("Ljava/util/NoSuchElementException;");
   public final DexString charSequenceDescriptor = createString("Ljava/lang/CharSequence;");
   public final DexString charSequenceArrayDescriptor = createString("[Ljava/lang/CharSequence;");
   public final DexString stringDescriptor = createString("Ljava/lang/String;");
@@ -575,6 +577,11 @@
   public final DexType runtimeExceptionType = createStaticallyKnownType(runtimeExceptionDescriptor);
   public final DexType assertionErrorType = createStaticallyKnownType(assertionErrorDescriptor);
   public final DexType throwableType = createStaticallyKnownType(throwableDescriptor);
+  public final DexType noSuchElementExceptionType =
+      createStaticallyKnownType(noSuchElementExceptionDescriptor);
+  public final DexMethod noSuchElementExceptionInit =
+      createInstanceInitializer(noSuchElementExceptionType);
+
   public final DexType illegalAccessErrorType =
       createStaticallyKnownType(illegalAccessErrorDescriptor);
   public final DexType illegalArgumentExceptionType =
@@ -870,6 +877,7 @@
 
   public final ObjectMethodsMembers objectMethodsMembers = new ObjectMethodsMembers();
   public final ServiceLoaderMethods serviceLoaderMethods = new ServiceLoaderMethods();
+  public final IteratorMethods iteratorMethods = new IteratorMethods();
   public final StringConcatFactoryMembers stringConcatFactoryMembers =
       new StringConcatFactoryMembers();
 
@@ -2659,6 +2667,12 @@
     }
   }
 
+  public class IteratorMethods {
+    public final DexMethod hasNext =
+        createMethod(iteratorType, createProto(booleanType), "hasNext");
+    public final DexMethod next = createMethod(iteratorType, createProto(objectType), "next");
+  }
+
   private static <T extends DexItem> T canonicalize(Map<T, T> map, T item) {
     assert item != null;
     assert !DexItemFactory.isInternalSentinel(item);
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 5077746..307acdf 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
@@ -1109,7 +1109,7 @@
     return new IRCodeInstructionIterator(this);
   }
 
-  public InstructionListIterator instructionListIterator() {
+  public IRCodeInstructionListIterator instructionListIterator() {
     return new IRCodeInstructionListIterator(this);
   }
 
@@ -1241,6 +1241,14 @@
     return createNumberConstant(Float.floatToIntBits(value), TypeElement.getFloat(), local);
   }
 
+  public ConstNumber createBooleanConstant(boolean value) {
+    return createBooleanConstant(value, null);
+  }
+
+  public ConstNumber createBooleanConstant(boolean value, DebugLocalInfo local) {
+    return createNumberConstant(value ? 1 : 0, TypeElement.getInt(), local);
+  }
+
   public ConstNumber createIntConstant(int value) {
     return createIntConstant(value, null);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
index 43c3b39..13e393a 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
@@ -35,6 +35,10 @@
     this.instructionIterator = blockIterator.next().listIterator(code);
   }
 
+  public IRCode getCode() {
+    return code;
+  }
+
   @Override
   public Value insertConstNumberInstruction(
       IRCode code, InternalOptions options, long value, TypeElement type) {
@@ -244,11 +248,25 @@
         code, blockIterator, instructionsToAdd, options);
   }
 
+  public void addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
+      Collection<? extends Instruction> instructionsToAdd, InternalOptions options) {
+    instructionIterator =
+        instructionIterator.addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
+            code, blockIterator, instructionsToAdd, options);
+  }
+
   @Override
   public void remove() {
     instructionIterator.remove();
   }
 
+  public void removeRemainingInBlockIgnoreOutValue() {
+    while (instructionIterator.hasNext()) {
+      instructionIterator.next();
+      instructionIterator.removeInstructionIgnoreOutValue();
+    }
+  }
+
   @Override
   public void set(Instruction instruction) {
     instructionIterator.set(instruction);
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
index 10d5150..c1e8aa6 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.Sets;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.ListIterator;
 import java.util.Set;
@@ -69,6 +70,15 @@
         code, blockIterator, Arrays.asList(instructionsToAdd), options);
   }
 
+  default InstructionListIterator addPossiblyThrowingInstructionToPossiblyThrowingBlock(
+      IRCode code,
+      BasicBlockIterator blockIterator,
+      Instruction instructionToAdd,
+      InternalOptions options) {
+    return addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
+        code, blockIterator, Collections.singleton(instructionToAdd), options);
+  }
+
   default void addAndPositionBeforeNewInstruction(Instruction instruction) {
     add(instruction);
     Instruction previous = previous();
diff --git a/src/main/java/com/android/tools/r8/ir/code/NextUntilIterator.java b/src/main/java/com/android/tools/r8/ir/code/NextUntilIterator.java
index d87fed8..8877b0e 100644
--- a/src/main/java/com/android/tools/r8/ir/code/NextUntilIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/NextUntilIterator.java
@@ -24,4 +24,19 @@
     }
     return null;
   }
+
+  /**
+   * Continue to call {@link #next} until it returns the given item.
+   *
+   * @returns item if it was found, null otherwise.
+   */
+  @SuppressWarnings({"TypeParameterUnusedInFormals", "unchecked"})
+  default <S extends T> S nextUntil(T item) {
+    while (hasNext()) {
+      if (next() == item) {
+        return (S) item;
+      }
+    }
+    return null;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
index 1764c38..d7bf459 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
@@ -12,29 +12,37 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory.IteratorMethods;
 import com.android.tools.r8.graph.DexItemFactory.ServiceLoaderMethods;
-import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.ConstClass;
 import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.IRCodeInstructionListIterator;
 import com.android.tools.r8.ir.code.Instruction;
-import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.InvokeStatic;
 import com.android.tools.r8.ir.code.InvokeVirtual;
+import com.android.tools.r8.ir.code.NewInstance;
+import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.code.Throw;
 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.desugar.ServiceLoaderSourceCode;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.BooleanBox;
+import com.android.tools.r8.utils.ConsumerUtils;
 import com.android.tools.r8.utils.ListUtils;
-import com.google.common.collect.ImmutableList;
+import com.android.tools.r8.utils.WorkList;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
@@ -70,11 +78,16 @@
 
   private final AndroidApiLevelCompute apiLevelCompute;
   private final ServiceLoaderMethods serviceLoaderMethods;
+  private final IteratorMethods iteratorMethods;
+  private final boolean hasAssumeNoSideEffects;
 
   public ServiceLoaderRewriter(AppView<?> appView) {
     super(appView);
     this.apiLevelCompute = appView.apiLevelCompute();
     this.serviceLoaderMethods = appView.dexItemFactory().serviceLoaderMethods;
+    this.iteratorMethods = appView.dexItemFactory().iteratorMethods;
+    hasAssumeNoSideEffects =
+        appView.getAssumeInfoCollection().isSideEffectFree(serviceLoaderMethods.load);
   }
 
   @Override
@@ -103,170 +116,396 @@
       IRCode code,
       MethodProcessor methodProcessor,
       MethodProcessingContext methodProcessingContext) {
-    InstructionListIterator instructionIterator = code.instructionListIterator();
     // Create a map from service type to loader methods local to this context since two
     // service loader calls to the same type in different methods and in the same wave can race.
     Map<DexType, DexEncodedMethod> synthesizedServiceLoaders = new IdentityHashMap<>();
-    while (instructionIterator.hasNext()) {
-      Instruction instruction = instructionIterator.next();
+    IRCodeInstructionListIterator iterator = code.instructionListIterator();
+    Map<Instruction, Instruction> replacements = new HashMap<>();
+    Map<Instruction, List<Instruction>> replacementExtras = new HashMap<>();
 
-      // Check if instruction is an invoke static on the desired form of ServiceLoader.load.
-      if (!instruction.isInvokeStatic()) {
+    while (iterator.hasNext()) {
+      Instruction instruction = iterator.next();
+
+      // Find ServiceLoader.load() calls.
+      InvokeStatic invokeInstr = instruction.asInvokeStatic();
+      if (invokeInstr == null
+          || !serviceLoaderMethods.isLoadMethod(invokeInstr.getInvokedMethod())) {
         continue;
       }
 
-      InvokeStatic serviceLoaderLoad = instruction.asInvokeStatic();
-      DexMethod invokedMethod = serviceLoaderLoad.getInvokedMethod();
-      if (!serviceLoaderMethods.isLoadMethod(invokedMethod)) {
+      // See if it can be optimized.
+      ServiceLoaderLoadResult loadResult = analyzeServiceLoaderLoad(code, invokeInstr);
+      if (loadResult == null) {
         continue;
       }
+      // See if there is a subsequent iterator() call that can be optimized.
+      DirectRewriteResult directRewriteResult = analyzeForDirectRewrite(loadResult);
 
-      // Check that the first argument is a const class.
-      Value argument = serviceLoaderLoad.getFirstArgument().getAliasedValue();
-      if (!argument.isDefinedByInstructionSatisfying(Instruction::isConstClass)) {
-        report(code.context(), null, "The service loader type could not be determined");
-        continue;
+      // Remove ServiceLoader.load()
+      replacements.put(loadResult.loadInvoke, code.createConstNull());
+      // Remove getClassLoader() if we are about to remove all users.
+      if (loadResult.classLoaderInvoke != null
+          && loadResult.classLoaderInvoke.outValue().aliasedUsers().stream()
+              .allMatch(ins -> ins.isAssume() || replacements.containsKey(ins))) {
+        replacements.put(loadResult.classLoaderInvoke, code.createConstNull());
       }
-
-      ConstClass constClass = argument.getDefinition().asConstClass();
-      DexType serviceType = constClass.getType();
-      if (invokedMethod.isNotIdenticalTo(serviceLoaderMethods.loadWithClassLoader)) {
-        report(
-            code.context(),
-            serviceType,
-            "Inlining is only supported for `java.util.ServiceLoader.load(java.lang.Class,"
-                + " java.lang.ClassLoader)`");
-        continue;
+      if (directRewriteResult != null) {
+        populateDirectRewriteChanges(
+            code, replacements, replacementExtras, loadResult, directRewriteResult);
+      } else {
+        populateSyntheticChanges(
+            code,
+            methodProcessor,
+            methodProcessingContext,
+            synthesizedServiceLoaders,
+            replacements,
+            loadResult);
       }
-
-      String invalidUserMessage =
-          "The returned ServiceLoader instance must only be used in a call to `java.util.Iterator"
-              + " java.lang.ServiceLoader.iterator()`";
-      Value serviceLoaderLoadOut = serviceLoaderLoad.outValue();
-      if (!serviceLoaderLoadOut.hasSingleUniqueUser() || serviceLoaderLoadOut.hasPhiUsers()) {
-        report(code.context(), serviceType, invalidUserMessage);
-        continue;
-      }
-
-      // Check that the only user is a call to iterator().
-      InvokeVirtual singleUniqueUser = serviceLoaderLoadOut.singleUniqueUser().asInvokeVirtual();
-      if (singleUniqueUser == null
-          || singleUniqueUser.getInvokedMethod().isNotIdenticalTo(serviceLoaderMethods.iterator)) {
-        report(code.context(), serviceType, invalidUserMessage + ", but found other usages");
-        continue;
-      }
-
-      // Check that the service is not kept.
-      if (appView().appInfo().isPinnedWithDefinitionLookup(constClass.getType())) {
-        report(code.context(), serviceType, "The service loader type is kept");
-        continue;
-      }
-
-      // Check that ClassLoader used is the ClassLoader defined for the service configuration
-      // that we are instantiating or NULL.
-      Value classLoaderValue = serviceLoaderLoad.getLastArgument().getAliasedValue();
-      if (classLoaderValue.isPhi()) {
-        report(
-            code.context(),
-            serviceType,
-            "The java.lang.ClassLoader argument must be defined locally as null or "
-                + serviceType
-                + ".class.getClassLoader()");
-        continue;
-      }
-      boolean isNullClassLoader = classLoaderValue.getType().isNullType();
-      InvokeVirtual classLoaderInvoke = classLoaderValue.getDefinition().asInvokeVirtual();
-      boolean isGetClassLoaderOnConstClass =
-          classLoaderInvoke != null
-              && classLoaderInvoke.arguments().size() == 1
-              && classLoaderInvoke.getReceiver().getAliasedValue().isConstClass()
-              && classLoaderInvoke
-                  .getReceiver()
-                  .getAliasedValue()
-                  .getDefinition()
-                  .asConstClass()
-                  .getType()
-                  .isIdenticalTo(serviceType);
-      if (!isNullClassLoader && !isGetClassLoaderOnConstClass) {
-        report(
-            code.context(),
-            serviceType,
-            "The java.lang.ClassLoader argument must be defined locally as null or "
-                + serviceType
-                + ".class.getClassLoader()");
-        continue;
-      }
-
-      // Check that the service is configured in the META-INF/services.
-      AppServices appServices = appView.appServices();
-      List<DexType> dexTypes = appServices.serviceImplementationsFor(constClass.getType());
-      List<DexClass> classes = new ArrayList<>(dexTypes.size());
-      for (DexType serviceImpl : dexTypes) {
-        DexClass serviceImplementation = appView.definitionFor(serviceImpl);
-        if (serviceImplementation == null) {
-          report(
-              code.context(),
-              serviceType,
-              "Unable to find definition for service implementation " + serviceImpl.getTypeName());
-          break;
-        }
-        if (!appView().appInfo().isSubtype(serviceImpl, serviceType)) {
-          report(
-              code.context(),
-              serviceType,
-              "Implementation is not a subtype of the service: " + serviceImpl.getTypeName());
-          break;
-        }
-        DexEncodedMethod method = serviceImplementation.getDefaultInitializer();
-        if (method == null) {
-          report(
-              code.context(),
-              serviceType,
-              "Implementation has no default constructor: " + serviceImpl.getTypeName());
-          break;
-        }
-        if (!method.getAccessFlags().wasPublic()) {
-          // A non-public constructor causes a ServiceConfigurationError on APIs 24 & 25 (Nougat).
-          // Check the original access flag to not change app behavior if R8 publicized the method.
-          report(
-              code.context(),
-              serviceType,
-              "Implementation's default constructor is not public: " + serviceImpl.getTypeName());
-          break;
-        }
-        classes.add(serviceImplementation);
-      }
-      if (dexTypes.size() != classes.size()) {
-        continue;
-      }
-
-      // Check that we are not service loading anything from a feature into base.
-      if (hasServiceImplementationInDifferentFeature(code, serviceType, isNullClassLoader)) {
-        continue;
-      }
-
-      // We can perform the rewrite of the ServiceLoader.load call.
-      DexEncodedMethod synthesizedMethod =
-          synthesizedServiceLoaders.computeIfAbsent(
-              constClass.getType(),
-              service -> {
-                DexEncodedMethod addedMethod =
-                    createSynthesizedMethod(
-                        service, classes, methodProcessor, methodProcessingContext);
-                if (appView.options().isGeneratingClassFiles()) {
-                  addedMethod.upgradeClassFileVersion(
-                      code.context().getDefinition().getClassFileVersion());
-                }
-                return addedMethod;
-              });
-
-      new Rewriter(code, instructionIterator, serviceLoaderLoad)
-          .perform(classLoaderInvoke, synthesizedMethod.getReference());
     }
+
+    if (replacements.isEmpty()) {
+      return CodeRewriterResult.NO_CHANGE;
+    }
+
+    AffectedValues affectedValues = new AffectedValues();
+    iterator = code.instructionListIterator();
+    while (iterator.hasNext()) {
+      Instruction instruction = iterator.next();
+      Instruction replacement = replacements.get(instruction);
+      if (replacement == null) {
+        continue;
+      }
+      iterator.replaceCurrentInstruction(replacement, affectedValues);
+      List<Instruction> extras = replacementExtras.get(instruction);
+      if (extras != null) {
+        iterator.addPossiblyThrowingInstructionsToPossiblyThrowingBlock(extras, options);
+        if (ListUtils.last(extras).isThrow()) {
+          iterator.removeRemainingInBlockIgnoreOutValue();
+        }
+      }
+    }
+
+    code.removeUnreachableBlocks(affectedValues, ConsumerUtils.emptyConsumer());
+    code.removeRedundantBlocks();
+    affectedValues.narrowingWithAssumeRemoval(appView, code);
     assert code.isConsistentSSA(appView);
-    return synthesizedServiceLoaders.isEmpty()
-        ? CodeRewriterResult.NO_CHANGE
-        : CodeRewriterResult.HAS_CHANGED;
+
+    return CodeRewriterResult.HAS_CHANGED;
+  }
+
+  private void populateSyntheticChanges(
+      IRCode code,
+      MethodProcessor methodProcessor,
+      MethodProcessingContext methodProcessingContext,
+      Map<DexType, DexEncodedMethod> synthesizedServiceLoaders,
+      Map<Instruction, Instruction> replacements,
+      ServiceLoaderLoadResult loadResult) {
+    DexEncodedMethod synthesizedMethod =
+        synthesizedServiceLoaders.computeIfAbsent(
+            loadResult.serviceType,
+            service -> {
+              DexEncodedMethod addedMethod =
+                  createSynthesizedMethod(
+                      service, loadResult.implClasses, methodProcessor, methodProcessingContext);
+              if (appView.options().isGeneratingClassFiles()) {
+                addedMethod.upgradeClassFileVersion(
+                    code.context().getDefinition().getClassFileVersion());
+              }
+              return addedMethod;
+            });
+    InvokeStatic synthesizedInvoke =
+        new InvokeStatic(
+            synthesizedMethod.getReference(),
+            loadResult.iteratorInvoke.outValue(),
+            Collections.emptyList());
+    replacements.put(loadResult.iteratorInvoke, synthesizedInvoke);
+  }
+
+  private void populateDirectRewriteChanges(
+      IRCode code,
+      Map<Instruction, Instruction> replacements,
+      Map<Instruction, List<Instruction>> replacementExtras,
+      ServiceLoaderLoadResult loadResult,
+      DirectRewriteResult directRewriteResult) {
+    // Remove ServiceLoader.iterator()
+    replacements.put(loadResult.iteratorInvoke, code.createConstNull());
+
+    // Iterator.hasNext() --> true / false
+    if (directRewriteResult.hasNextInstr != null) {
+      replacements.put(
+          directRewriteResult.hasNextInstr,
+          code.createBooleanConstant(!loadResult.implClasses.isEmpty()));
+    }
+    if (directRewriteResult.nextInstr != null) {
+      Position position = directRewriteResult.nextInstr.getPosition();
+      if (loadResult.implClasses.isEmpty()) {
+        // Iterator.next() -> null
+        replacements.put(directRewriteResult.nextInstr, code.createConstNull());
+
+        // throw new NoSuchElementException()
+        NewInstance newInstanceInstr =
+            new NewInstance(
+                dexItemFactory.noSuchElementExceptionType,
+                code.createValue(
+                    dexItemFactory.noSuchElementExceptionType.toNonNullTypeElement(appView)));
+        InvokeDirect initInstr =
+            new InvokeDirect(
+                dexItemFactory.noSuchElementExceptionInit,
+                null,
+                List.of(newInstanceInstr.outValue()));
+        Throw throwInstr = new Throw(newInstanceInstr.outValue());
+
+        newInstanceInstr.setPosition(position);
+        initInstr.setPosition(position);
+        throwInstr.setPosition(position);
+        replacementExtras.put(
+            directRewriteResult.nextInstr, List.of(newInstanceInstr, initInstr, throwInstr));
+      } else {
+        // Iterator.next() -> new ServiceImpl()
+        DexType clazz = loadResult.implClasses.get(0).getType();
+        NewInstance newInstance =
+            new NewInstance(
+                clazz,
+                code.createValue(
+                    clazz.toNonNullTypeElement(appView),
+                    directRewriteResult.nextInstr.getLocalInfo()));
+        replacements.put(directRewriteResult.nextInstr, newInstance);
+        InvokeDirect initInstr =
+            new InvokeDirect(
+                dexItemFactory.createInstanceInitializer(clazz),
+                null,
+                List.of(newInstance.outValue()));
+        initInstr.setPosition(position);
+        replacementExtras.put(directRewriteResult.nextInstr, List.of(initInstr));
+      }
+    }
+  }
+
+  private ServiceLoaderLoadResult analyzeServiceLoaderLoad(IRCode code, InvokeStatic invokeInstr) {
+    // Check that the first argument is a const class.
+    Value argument = invokeInstr.getFirstArgument().getAliasedValue();
+    if (!argument.isDefinedByInstructionSatisfying(Instruction::isConstClass)) {
+      report(code.context(), null, "The service loader type could not be determined");
+      return null;
+    }
+
+    ConstClass constClass = argument.getDefinition().asConstClass();
+    DexType serviceType = constClass.getType();
+    if (invokeInstr.getInvokedMethod().isNotIdenticalTo(serviceLoaderMethods.loadWithClassLoader)) {
+      report(
+          code.context(),
+          serviceType,
+          "Inlining is only supported for `java.util.ServiceLoader.load(java.lang.Class,"
+              + " java.lang.ClassLoader)`");
+      return null;
+    }
+
+    String invalidUserMessage =
+        "The returned ServiceLoader instance must only be used in a call to `java.util.Iterator"
+            + " java.lang.ServiceLoader.iterator()`";
+    Value serviceLoaderLoadOut = invokeInstr.outValue();
+    if (!serviceLoaderLoadOut.hasSingleUniqueUser() || serviceLoaderLoadOut.hasPhiUsers()) {
+      report(code.context(), serviceType, invalidUserMessage);
+      return null;
+    }
+
+    // Check that the only user is a call to iterator().
+    InvokeVirtual iteratorInvoke = serviceLoaderLoadOut.singleUniqueUser().asInvokeVirtual();
+    if (iteratorInvoke == null
+        || iteratorInvoke.getInvokedMethod().isNotIdenticalTo(serviceLoaderMethods.iterator)) {
+      report(code.context(), serviceType, invalidUserMessage + ", but found other usages");
+      return null;
+    }
+
+    // Check that the service is not kept.
+    if (appView().appInfo().isPinnedWithDefinitionLookup(serviceType)) {
+      report(code.context(), serviceType, "The service loader type is kept");
+      return null;
+    }
+
+    // Check that ClassLoader used is the ClassLoader defined for the service configuration
+    // that we are instantiating or NULL.
+    Value classLoaderValue = invokeInstr.getLastArgument().getAliasedValue();
+    if (classLoaderValue.isPhi()) {
+      report(
+          code.context(),
+          serviceType,
+          "The java.lang.ClassLoader argument must be defined locally as null or "
+              + serviceType
+              + ".class.getClassLoader()");
+      return null;
+    }
+    boolean isNullClassLoader = classLoaderValue.getType().isNullType();
+    InvokeVirtual classLoaderInvoke = classLoaderValue.getDefinition().asInvokeVirtual();
+    boolean isGetClassLoaderOnConstClass =
+        classLoaderInvoke != null
+            && classLoaderInvoke.arguments().size() == 1
+            && classLoaderInvoke.getReceiver().getAliasedValue().isConstClass()
+            && classLoaderInvoke
+                .getReceiver()
+                .getAliasedValue()
+                .getDefinition()
+                .asConstClass()
+                .getType()
+                .isIdenticalTo(serviceType);
+    if (!isNullClassLoader && !isGetClassLoaderOnConstClass) {
+      report(
+          code.context(),
+          serviceType,
+          "The java.lang.ClassLoader argument must be defined locally as null or "
+              + serviceType
+              + ".class.getClassLoader()");
+      return null;
+    }
+
+    // Check that we are not service loading anything from a feature into base.
+    AppServices appServices = appView.appServices();
+    // Check that we are not service loading anything from a feature into base.
+    if (hasServiceImplementationInDifferentFeature(code, serviceType, isNullClassLoader)) {
+      return null;
+    }
+
+    List<DexType> dexTypes = appServices.serviceImplementationsFor(serviceType);
+    List<DexClass> implClasses = new ArrayList<>(dexTypes.size());
+    for (DexType serviceImpl : dexTypes) {
+      DexClass serviceImplementation = appView.definitionFor(serviceImpl);
+      if (serviceImplementation == null) {
+        report(
+            code.context(),
+            serviceType,
+            "Unable to find definition for service implementation " + serviceImpl.getTypeName());
+        return null;
+      }
+      if (appView.isSubtype(serviceImpl, serviceType).isFalse()) {
+        report(
+            code.context(),
+            serviceType,
+            "Implementation is not a subtype of the service: " + serviceImpl.getTypeName());
+        return null;
+      }
+      DexEncodedMethod method = serviceImplementation.getDefaultInitializer();
+      if (method == null) {
+        report(
+            code.context(),
+            serviceType,
+            "Implementation has no default constructor: " + serviceImpl.getTypeName());
+        return null;
+      }
+      if (!method.getAccessFlags().wasPublic()) {
+        // A non-public constructor causes a ServiceConfigurationError on APIs 24 & 25 (Nougat).
+        report(
+            code.context(),
+            serviceType,
+            "Implementation's default constructor is not public: " + serviceImpl.getTypeName());
+        return null;
+      }
+      implClasses.add(serviceImplementation);
+    }
+
+    return new ServiceLoaderLoadResult(
+        invokeInstr, classLoaderInvoke, serviceType, implClasses, iteratorInvoke);
+  }
+
+  /**
+   * Checks that:
+   *
+   * <pre>
+   *   * -assumenosideeffects for ServiceLoader.load() is set.
+   *   * ServiceLoader.iterator() is used only by a single .hasNext() and/or .next()
+   *   * .iterator(), .hasNext(), and .next() are all in the same try/catch.
+   *   * .hasNext() never comes after a call to .next()
+   *   * .hasNext() and .next() are not in a loop.
+   * </pre>
+   */
+  private DirectRewriteResult analyzeForDirectRewrite(ServiceLoaderLoadResult loadResult) {
+    // Require -assumenosideeffects class java.util.ServiceLoader { java.lang.Object load(...); }
+    // because this direct rewriting does not wrap exceptions in ServiceConfigurationError.
+    if (!hasAssumeNoSideEffects) {
+      return null;
+    }
+    InvokeVirtual iteratorInvoke = loadResult.iteratorInvoke;
+
+    if (iteratorInvoke.outValue().hasPhiUsers()) {
+      return null;
+    }
+    InvokeMethod hasNextInstr = null;
+    InvokeMethod nextInstr = null;
+    // We only bother to support a single call to hasNext() and next(), and they must appear within
+    // the same try/catch.
+    for (Instruction user : iteratorInvoke.outValue().aliasedUsers()) {
+      if (user.isAssume()) {
+        if (user.outValue().hasPhiUsers()) {
+          return null;
+        }
+        continue;
+      }
+      if (!user.getBlock().hasEquivalentCatchHandlers(iteratorInvoke.getBlock())) {
+        return null;
+      }
+      InvokeMethod curCall = user.asInvokeMethod();
+      if (curCall == null) {
+        return null;
+      }
+      if (curCall.getInvokedMethod().isIdenticalTo(iteratorMethods.hasNext)) {
+        if (hasNextInstr != null) {
+          return null;
+        }
+        hasNextInstr = curCall;
+      } else if (curCall.getInvokedMethod().isIdenticalTo(iteratorMethods.next)) {
+        if (nextInstr != null) {
+          return null;
+        }
+        nextInstr = curCall;
+      } else {
+        return null;
+      }
+    }
+
+    BasicBlock iteratorBlock = iteratorInvoke.getBlock();
+    BasicBlock hasNextBlock = hasNextInstr != null ? hasNextInstr.getBlock() : null;
+    BasicBlock nextBlock = nextInstr != null ? nextInstr.getBlock() : null;
+    if (hasNextBlock != null && nextBlock != null) {
+      // See if hasNext() is reachable after next().
+      if (hasNextBlock == nextBlock) {
+        if (nextBlock.iterator(nextInstr).nextUntil(hasNextInstr) != null) {
+          return null;
+        }
+      } else if (hasPredecessorPathTo(iteratorBlock, hasNextBlock, nextBlock)) {
+        return null;
+      }
+    }
+
+    // Make sure each instruction can be run at most once (no loops).
+    if (hasNextBlock != null && loopExists(iteratorBlock, hasNextBlock)) {
+      return null;
+    }
+    if (nextBlock != null && loopExists(iteratorBlock, nextBlock)) {
+      return null;
+    }
+
+    return new DirectRewriteResult(hasNextInstr, nextInstr);
+  }
+
+  private static boolean loopExists(BasicBlock subgraphEntryBlock, BasicBlock targetBlock) {
+    return hasPredecessorPathTo(subgraphEntryBlock, targetBlock, targetBlock);
+  }
+
+  private static boolean hasPredecessorPathTo(
+      BasicBlock subgraphEntryBlock, BasicBlock subgraphExitBlock, BasicBlock targetBlock) {
+    if (subgraphEntryBlock == subgraphExitBlock) {
+      return false;
+    }
+    WorkList<BasicBlock> workList = WorkList.newIdentityWorkList();
+    workList.markAsSeen(subgraphEntryBlock);
+    workList.addIfNotSeen(subgraphExitBlock.getPredecessors());
+    while (workList.hasNext()) {
+      BasicBlock curBlock = workList.next();
+      if (curBlock == targetBlock) {
+        return true;
+      }
+      workList.addIfNotSeen(curBlock.getPredecessors());
+    }
+    return false;
   }
 
   private void report(ProgramMethod method, DexType serviceLoaderType, String message) {
@@ -375,77 +614,34 @@
     return method.getDefinition();
   }
 
-  /**
-   * Rewriter assumes that the code is of the form:
-   *
-   * <pre>
-   * ConstClass         v1 <- X
-   * ConstClass         v2 <- X or NULL
-   * Invoke-Virtual     v3 <- v2; method: java.lang.ClassLoader java.lang.Class.getClassLoader()
-   * Invoke-Static      v4 <- v1, v3; method: java.util.ServiceLoader java.util.ServiceLoader
-   *     .load(java.lang.Class, java.lang.ClassLoader)
-   * Invoke-Virtual     v5 <- v4; method: java.util.Iterator java.util.ServiceLoader.iterator()
-   * </pre>
-   *
-   * and rewrites it to:
-   *
-   * <pre>
-   * Invoke-Static      v5 <- ; method: java.util.Iterator syn(X)()
-   * </pre>
-   *
-   * where syn(X) is the synthesized method generated for the service class.
-   *
-   * <p>We rely on the DeadCodeRemover to remove the ConstClasses and any aliased values no longer
-   * used.
-   */
-  private static class Rewriter {
+  private static class ServiceLoaderLoadResult {
+    public final InvokeStatic loadInvoke;
+    public final InvokeVirtual classLoaderInvoke;
+    public final DexType serviceType;
+    public final List<DexClass> implClasses;
+    public final InvokeVirtual iteratorInvoke;
 
-    private final IRCode code;
-    private final InvokeStatic serviceLoaderLoad;
-
-    private final InstructionListIterator iterator;
-
-    Rewriter(IRCode code, InstructionListIterator iterator, InvokeStatic serviceLoaderLoad) {
-      this.iterator = iterator;
-      this.code = code;
-      this.serviceLoaderLoad = serviceLoaderLoad;
-    }
-
-    public void perform(InvokeVirtual classLoaderInvoke, DexMethod method) {
-      // Remove the ClassLoader call since this can throw and will not be removed otherwise.
-      if (classLoaderInvoke != null) {
-        BooleanBox allClassLoaderUsersAreServiceLoaders =
-            new BooleanBox(!classLoaderInvoke.outValue().hasPhiUsers());
-        classLoaderInvoke
-            .outValue()
-            .aliasedUsers()
-            .forEach(user -> allClassLoaderUsersAreServiceLoaders.and(user == serviceLoaderLoad));
-        if (allClassLoaderUsersAreServiceLoaders.get()) {
-          clearGetClassLoader(classLoaderInvoke);
-          iterator.nextUntil(i -> i == serviceLoaderLoad);
-        }
+    public ServiceLoaderLoadResult(
+        InvokeStatic loadInvoke,
+        InvokeVirtual classLoaderInvoke,
+        DexType serviceType,
+        List<DexClass> implClasses,
+        InvokeVirtual iteratorInvoke) {
+      this.loadInvoke = loadInvoke;
+      this.classLoaderInvoke = classLoaderInvoke;
+      this.serviceType = serviceType;
+      this.implClasses = implClasses;
+      this.iteratorInvoke = iteratorInvoke;
       }
+  }
 
-      // Remove the ServiceLoader.load call.
-      InvokeVirtual serviceLoaderIterator =
-          serviceLoaderLoad.outValue().singleUniqueUser().asInvokeVirtual();
-      iterator.replaceCurrentInstruction(code.createConstNull());
+  private static class DirectRewriteResult {
+    public final InvokeMethod nextInstr;
+    public final InvokeMethod hasNextInstr;
 
-      // Find the iterator instruction and replace it.
-      iterator.nextUntil(x -> x == serviceLoaderIterator);
-      InvokeStatic synthesizedInvoke =
-          new InvokeStatic(method, serviceLoaderIterator.outValue(), ImmutableList.of());
-      iterator.replaceCurrentInstruction(synthesizedInvoke);
-    }
-
-    private void clearGetClassLoader(InvokeVirtual classLoaderInvoke) {
-      while (iterator.hasPrevious()) {
-        Instruction instruction = iterator.previous();
-        if (instruction == classLoaderInvoke) {
-          iterator.replaceCurrentInstruction(code.createConstNull());
-          break;
-        }
-      }
+    public DirectRewriteResult(InvokeMethod hasNextInstr, InvokeMethod nextInstr) {
+      this.hasNextInstr = hasNextInstr;
+      this.nextInstr = nextInstr;
     }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderMultipleCallsSameMethodTest.java b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderMultipleCallsSameMethodTest.java
index 111badc..5cd038d 100644
--- a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderMultipleCallsSameMethodTest.java
+++ b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderMultipleCallsSameMethodTest.java
@@ -76,10 +76,10 @@
         .assertSuccessWithOutput(EXPECTED_OUTPUT)
         .inspect(
             inspector -> {
-              verifyNoServiceLoaderLoads(inspector);
+              verifyNoServiceLoaderLoads(inspector.clazz(MainRunner.class));
               verifyNoClassLoaders(inspector);
               verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
-              // Check the synthesize service loader method is a single shared method.
+              // Check the synthesized service loader method is a single shared method.
               // Due to minification we just check there is only a single synthetic class with a
               // single static method.
               boolean found = false;
diff --git a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingWithAssumeNoSideEffectsTest.java b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingWithAssumeNoSideEffectsTest.java
new file mode 100644
index 0000000..c57f579
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingWithAssumeNoSideEffectsTest.java
@@ -0,0 +1,278 @@
+// 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.optimize.serviceloader;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.ServiceLoader;
+import java.util.concurrent.ExecutionException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ServiceLoaderRewritingWithAssumeNoSideEffectsTest extends ServiceLoaderTestBase {
+  private final String EXPECTED_OUTPUT = StringUtils.lines("Hello World!");
+
+  public interface Service {
+
+    void print();
+  }
+
+  public static class ServiceImpl implements Service {
+
+    @Override
+    public void print() {
+      System.out.println("Hello World!");
+    }
+  }
+
+  public static class ServiceImpl2 implements Service {
+
+    @Override
+    public void print() {
+      System.out.println("Hello World 2!");
+    }
+  }
+
+  public static class MainRunner {
+    public static void main(String[] args) {
+      ServiceLoader.load(Service.class, Service.class.getClassLoader()).iterator().next().print();
+    }
+  }
+
+  public static class HasNextRunner {
+    public static void main(String[] args) {
+      Iterator<Service> iterator =
+          ServiceLoader.load(Service.class, Service.class.getClassLoader()).iterator();
+      if (iterator.hasNext()) {
+        iterator.next().print();
+      }
+    }
+  }
+
+  public static class MultipleCallsRunner {
+    public static void main(String[] args) {
+      Iterator<Service> iterator =
+          ServiceLoader.load(Service.class, Service.class.getClassLoader()).iterator();
+      if (iterator.hasNext() && iterator.hasNext()) {
+        iterator.next().print();
+      }
+    }
+  }
+
+  public static class HasNextAfterNextRunner {
+    public static void main(String[] args) {
+      Iterator<Service> iterator =
+          ServiceLoader.load(Service.class, Service.class.getClassLoader()).iterator();
+
+      iterator.next().print();
+      if (iterator.hasNext()) {
+        System.out.println("not reached");
+      }
+    }
+  }
+
+  public static class HasNextAfterNextWithTryCatch {
+    public static void main(String[] args) {
+      try {
+        Iterator<Service> iterator =
+            ServiceLoader.load(Service.class, Service.class.getClassLoader()).iterator();
+
+        iterator.next().print();
+        if (iterator.hasNext()) {
+          System.out.println("not reached");
+        }
+      } catch (Throwable t) {
+        System.out.println("unreachable");
+      }
+    }
+  }
+
+  public static class LoopingRunner {
+    public static void main(String[] args) {
+      Iterator<Service> iterator =
+          ServiceLoader.load(Service.class, Service.class.getClassLoader()).iterator();
+      // Loop without a call to hasNext().
+      for (int i = System.currentTimeMillis() > 0 ? 0 : 1; i < 1; ++i) {
+        iterator.next().print();
+      }
+    }
+  }
+
+  public static class PhiRunner {
+    public static void main(String[] args) {
+      Iterator<Service> iterator =
+          System.currentTimeMillis() > 0
+              ? ServiceLoader.load(Service.class, Service.class.getClassLoader()).iterator()
+              : null;
+      iterator.next().print();
+    }
+  }
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public ServiceLoaderRewritingWithAssumeNoSideEffectsTest(TestParameters parameters) {
+    super(parameters);
+  }
+
+  private static void assertIteratorPresent(CodeInspector inspector, boolean expected) {
+    assertEquals(0, getServiceLoaderLoads(inspector));
+
+    boolean hasIteratorCall =
+        inspector
+            .streamInstructions()
+            .anyMatch(ins -> ins.isInvoke() && ins.getMethod().name.toString().equals("iterator"));
+    assertEquals(expected, hasIteratorCall);
+  }
+
+  private R8FullTestBuilder doTest(Class<?>... implClasses) throws IOException {
+    return serviceLoaderTest(Service.class, implClasses)
+        .addKeepRules("-assumenosideeffects class java.util.ServiceLoader { *** load(...); }");
+  }
+
+  @Test
+  public void testRewritingWithNoImplsHasNext()
+      throws IOException, CompilationFailedException, ExecutionException {
+    doTest()
+        .addKeepMainRule(HasNextRunner.class)
+        .compile()
+        .run(parameters.getRuntime(), HasNextRunner.class)
+        .assertSuccessWithOutput("")
+        .inspect(inspector -> assertIteratorPresent(inspector, false));
+  }
+
+  @Test
+  public void testRewritingWithMultiple()
+      throws IOException, CompilationFailedException, ExecutionException {
+    doTest(ServiceImpl.class, ServiceImpl2.class)
+        .addKeepMainRule(MainRunner.class)
+        .compile()
+        .run(parameters.getRuntime(), MainRunner.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT)
+        .inspect(
+            inspector -> {
+              assertIteratorPresent(inspector, false);
+              // ServiceImpl2 gets removed since next() is called only once.
+              verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+            });
+  }
+
+  @Test
+  public void testRewritingWithHasNext()
+      throws IOException, CompilationFailedException, ExecutionException {
+    doTest(ServiceImpl.class)
+        .addKeepMainRule(HasNextRunner.class)
+        .compile()
+        .run(parameters.getRuntime(), HasNextRunner.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT)
+        .inspect(
+            inspector -> {
+              assertIteratorPresent(inspector, false);
+              verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+            });
+  }
+
+  @Test
+  public void testDoNotRewriteMultipleCalls()
+      throws IOException, CompilationFailedException, ExecutionException {
+    doTest(ServiceImpl.class)
+        .addKeepMainRule(MultipleCallsRunner.class)
+        .compile()
+        .run(parameters.getRuntime(), MultipleCallsRunner.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT)
+        .inspect(
+            inspector -> {
+              assertIteratorPresent(inspector, true);
+              verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+            });
+  }
+
+  @Test
+  public void testDoNotRewriteHasNextAfterNext()
+      throws IOException, CompilationFailedException, ExecutionException {
+    doTest(ServiceImpl.class)
+        .addKeepMainRule(HasNextAfterNextRunner.class)
+        .compile()
+        .run(parameters.getRuntime(), HasNextAfterNextRunner.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT)
+        .inspect(
+            inspector -> {
+              assertIteratorPresent(inspector, true);
+              verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+            });
+  }
+
+  @Test
+  public void testDoNotRewriteHasNextAfterNextWithTryCatch()
+      throws IOException, CompilationFailedException, ExecutionException {
+    doTest(ServiceImpl.class)
+        .addKeepMainRule(HasNextAfterNextWithTryCatch.class)
+        .compile()
+        .run(parameters.getRuntime(), HasNextAfterNextWithTryCatch.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT)
+        .inspect(
+            inspector -> {
+              assertIteratorPresent(inspector, true);
+              verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+            });
+  }
+
+  @Test
+  public void testDoNotRewriteHasNextAfterNextBlocks()
+      throws IOException, CompilationFailedException, ExecutionException {
+    doTest(ServiceImpl.class)
+        .addKeepMainRule(HasNextAfterNextRunner.class)
+        .compile()
+        .run(parameters.getRuntime(), HasNextAfterNextRunner.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT)
+        .inspect(
+            inspector -> {
+              assertIteratorPresent(inspector, true);
+              verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+            });
+  }
+
+  @Test
+  public void testDoNotRewriteLoop()
+      throws IOException, CompilationFailedException, ExecutionException {
+    doTest(ServiceImpl.class)
+        .addKeepMainRule(LoopingRunner.class)
+        .compile()
+        .run(parameters.getRuntime(), LoopingRunner.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT)
+        .inspect(
+            inspector -> {
+              assertIteratorPresent(inspector, true);
+              verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+            });
+  }
+
+  @Test
+  public void testDoNotRewritePhiUser()
+      throws IOException, CompilationFailedException, ExecutionException {
+    doTest(ServiceImpl.class)
+        .addKeepMainRule(PhiRunner.class)
+        .compile()
+        .run(parameters.getRuntime(), PhiRunner.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT)
+        .inspect(
+            inspector -> {
+              assertIteratorPresent(inspector, true);
+              verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+            });
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderTestBase.java b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderTestBase.java
index edb1c4e..118a3fc 100644
--- a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderTestBase.java
+++ b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderTestBase.java
@@ -67,10 +67,6 @@
         method -> assertThat(method, not(invokesMethodWithName("getClassLoader"))));
   }
 
-  public static void verifyNoServiceLoaderLoads(CodeInspector inspector) {
-    inspector.allClasses().forEach(ServiceLoaderTestBase::verifyNoServiceLoaderLoads);
-  }
-
   public static void verifyNoServiceLoaderLoads(ClassSubject classSubject) {
     assertTrue(classSubject.isPresent());
     classSubject.forAllMethods(method -> assertThat(method, not(invokesMethodWithName("load"))));