Merge commit 'edb4e36cad16eea3915ae78f017f934c14604b44' into dev-release
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index d667575..a12020f 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -483,7 +483,6 @@
     assert !internal.inlinerOptions().enableInlining;
     assert !internal.enableClassInlining;
     assert !internal.enableVerticalClassMerging;
-    assert !internal.enableClassStaticizer;
     assert !internal.enableEnumValueOptimization;
     assert !internal.outline.enabled;
     assert !internal.enableTreeShakingOfLibraryMethodOverrides;
diff --git a/src/main/java/com/android/tools/r8/L8Command.java b/src/main/java/com/android/tools/r8/L8Command.java
index c39a041..41f4030 100644
--- a/src/main/java/com/android/tools/r8/L8Command.java
+++ b/src/main/java/com/android/tools/r8/L8Command.java
@@ -179,7 +179,6 @@
     assert !internal.inlinerOptions().enableInlining;
     assert !internal.enableClassInlining;
     assert !internal.enableVerticalClassMerging;
-    assert !internal.enableClassStaticizer;
     assert !internal.enableEnumValueOptimization;
     assert !internal.outline.enabled;
     assert !internal.enableTreeShakingOfLibraryMethodOverrides;
diff --git a/src/main/java/com/android/tools/r8/graph/GraphLens.java b/src/main/java/com/android/tools/r8/graph/GraphLens.java
index b27f94a..3e235b4 100644
--- a/src/main/java/com/android/tools/r8/graph/GraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/GraphLens.java
@@ -530,6 +530,10 @@
     return false;
   }
 
+  public boolean isArgumentPropagatorGraphLens() {
+    return false;
+  }
+
   public boolean isClearCodeRewritingLens() {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldReadForInvokeReceiverAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldReadForInvokeReceiverAnalysis.java
index b2aded5..976020c6 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldReadForInvokeReceiverAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldReadForInvokeReceiverAnalysis.java
@@ -74,6 +74,7 @@
         }
 
         methods.add(singleTarget.getReference());
+        continue;
       }
       return null;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java
index 7f36c0c..f13bd76 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java
@@ -333,7 +333,7 @@
       }
 
       if (metadata != null) {
-        if (isUnusedReadAfterMethodStaticizing(metadata)) {
+        if (isUnusedReadAfterMethodStaticizing(field, metadata)) {
           // Ignore this read.
           dependencies
               .computeIfAbsent(field.getDefinition(), ignoreKey(ProgramMethodSet::createConcurrent))
@@ -367,8 +367,10 @@
       }
     }
 
-    private boolean isUnusedReadAfterMethodStaticizing(BytecodeInstructionMetadata metadata) {
-      if (!metadata.isReadForInvokeReceiver()) {
+    private boolean isUnusedReadAfterMethodStaticizing(
+        DexClassAndField field, BytecodeInstructionMetadata metadata) {
+      if (!metadata.isReadForInvokeReceiver()
+          || field.getOptimizationInfo().getDynamicType().getNullability().isMaybeNull()) {
         return false;
       }
       Set<DexMethod> readForInvokeReceiver = metadata.getReadForInvokeReceiver();
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/StatefulObjectValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/StatefulObjectValue.java
index edcfbc6..2d090e9 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/StatefulObjectValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/StatefulObjectValue.java
@@ -72,7 +72,10 @@
 
   @Override
   public boolean equals(Object o) {
-    if (getClass() != o.getClass()) {
+    if (o == this) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
       return false;
     }
     StatefulObjectValue statefulObjectValue = (StatefulObjectValue) o;
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 e44b158..6a237d0 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
@@ -44,11 +44,13 @@
 import java.util.Deque;
 import java.util.HashSet;
 import java.util.IdentityHashMap;
+import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Map;
+import java.util.NoSuchElementException;
 import java.util.Queue;
 import java.util.Set;
 import java.util.function.Consumer;
@@ -1085,20 +1087,41 @@
         + BooleanUtils.intValue(!context().getDefinition().isStatic());
   }
 
+  public Iterator<Argument> argumentIterator() {
+    return new Iterator<Argument>() {
+
+      private final InstructionIterator instructionIterator = entryBlock().iterator();
+      private Argument next = instructionIterator.next().asArgument();
+
+      @Override
+      public boolean hasNext() {
+        return next != null;
+      }
+
+      @Override
+      public Argument next() {
+        if (next == null) {
+          throw new NoSuchElementException();
+        }
+        Argument previous = next;
+        next = instructionIterator.next().asArgument();
+        return previous;
+      }
+    };
+  }
+
   public List<Value> collectArguments() {
     return collectArguments(false);
   }
 
   public List<Value> collectArguments(boolean ignoreReceiver) {
     final List<Value> arguments = new ArrayList<>();
-    InstructionIterator iterator = entryBlock().iterator();
+    Iterator<Argument> iterator = argumentIterator();
     while (iterator.hasNext()) {
-      Instruction instruction = iterator.next();
-      if (instruction.isArgument()) {
-        Value out = instruction.asArgument().outValue();
-        if (!ignoreReceiver || !out.isThis()) {
-          arguments.add(out);
-        }
+      Argument argument = iterator.next();
+      Value out = argument.outValue();
+      if (!ignoreReceiver || !out.isThis()) {
+        arguments.add(out);
       }
     }
     assert arguments.size()
diff --git a/src/main/java/com/android/tools/r8/ir/code/Position.java b/src/main/java/com/android/tools/r8/ir/code/Position.java
index 9fd0c74..5de6390 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Position.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Position.java
@@ -180,6 +180,16 @@
         .build();
   }
 
+  public Position replaceOutermostCallerPosition(Position newOutermostCallerPosition) {
+    if (!hasCallerPosition()) {
+      return newOutermostCallerPosition;
+    }
+    return builderWithCopy()
+        .setCallerPosition(
+            getCallerPosition().replaceOutermostCallerPosition(newOutermostCallerPosition))
+        .build();
+  }
+
   @Override
   public final boolean equals(Object other) {
     return Equatable.equalsImpl(this, other);
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 45758d3..c19a35b 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
@@ -89,7 +89,6 @@
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackSimple;
 import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfoCollection;
 import com.android.tools.r8.ir.optimize.outliner.Outliner;
-import com.android.tools.r8.ir.optimize.staticizer.ClassStaticizer;
 import com.android.tools.r8.ir.optimize.string.StringBuilderOptimizer;
 import com.android.tools.r8.ir.optimize.string.StringOptimizer;
 import com.android.tools.r8.ir.regalloc.LinearScanRegisterAllocator;
@@ -104,7 +103,6 @@
 import com.android.tools.r8.shaking.LibraryMethodOverrideAnalysis;
 import com.android.tools.r8.utils.Action;
 import com.android.tools.r8.utils.CfgPrinter;
-import com.android.tools.r8.utils.ConsumerUtils;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions;
@@ -141,7 +139,6 @@
   private final StringBuilderOptimizer stringBuilderOptimizer;
   private final IdempotentFunctionCallCanonicalizer idempotentFunctionCallCanonicalizer;
   private final ClassInliner classInliner;
-  private final ClassStaticizer classStaticizer;
   private final InternalOptions options;
   private final CfgPrinter printer;
   public final CodeRewriter codeRewriter;
@@ -228,7 +225,6 @@
       this.covariantReturnTypeAnnotationTransformer = null;
       this.dynamicTypeOptimization = null;
       this.classInliner = null;
-      this.classStaticizer = null;
       this.fieldAccessAnalysis = null;
       this.libraryMethodOverrideAnalysis = null;
       this.inliner = null;
@@ -263,8 +259,6 @@
           options.enableClassInlining && options.inlinerOptions().enableInlining
               ? new ClassInliner()
               : null;
-      this.classStaticizer =
-          options.enableClassStaticizer ? new ClassStaticizer(appViewWithLiveness, this) : null;
       this.dynamicTypeOptimization = new DynamicTypeOptimization(appViewWithLiveness);
       this.fieldAccessAnalysis = new FieldAccessAnalysis(appViewWithLiveness);
       this.libraryMethodOverrideAnalysis =
@@ -295,7 +289,6 @@
     } else {
       this.assumeInserter = null;
       this.classInliner = null;
-      this.classStaticizer = null;
       this.dynamicTypeOptimization = null;
       this.fieldAccessAnalysis = null;
       this.libraryMethodOverrideAnalysis = null;
@@ -350,13 +343,6 @@
         D8NestBasedAccessDesugaring::clearNestAttributes);
   }
 
-  private void staticizeClasses(OptimizationFeedback feedback, ExecutorService executorService)
-      throws ExecutionException {
-    if (classStaticizer != null) {
-      classStaticizer.staticizeCandidates(feedback, executorService);
-    }
-  }
-
   private void processCovariantReturnTypeAnnotations(Builder<?> builder) {
     if (covariantReturnTypeAnnotationTransformer != null) {
       covariantReturnTypeAnnotationTransformer.process(builder);
@@ -652,10 +638,6 @@
     appView.withArgumentPropagator(
         argumentPropagator -> argumentPropagator.initializeCodeScanner(executorService, timing));
     enumUnboxer.prepareForPrimaryOptimizationPass(graphLensForPrimaryOptimizationPass);
-    ConsumerUtils.acceptIfNotNull(
-        classStaticizer,
-        classStaticizer ->
-            classStaticizer.prepareForPrimaryOptimizationPass(graphLensForPrimaryOptimizationPass));
     outliner.prepareForPrimaryOptimizationPass(graphLensForPrimaryOptimizationPass);
 
     if (fieldAccessAnalysis != null) {
@@ -727,11 +709,6 @@
 
     GraphLens graphLensForSecondaryOptimizationPass = appView.graphLens();
 
-    ConsumerUtils.acceptIfNotNull(
-        classStaticizer,
-        classStaticizer ->
-            classStaticizer.prepareForSecondaryOptimizationPass(
-                graphLensForSecondaryOptimizationPass));
     outliner.rewriteWithLens();
 
     timing.begin("IR conversion phase 2");
@@ -765,17 +742,6 @@
     // staticizer and phase 3 does not perform again the rewriting.
     appView.clearCodeRewritings();
 
-    // TODO(b/112831361): Implement support for staticizeClasses in CF backend.
-    if (!options.isGeneratingClassFiles()) {
-      printPhase("Class staticizer post processing");
-      // TODO(b/127694949): Adapt to PostOptimization.
-      staticizeClasses(feedback, executorService);
-      feedback.updateVisibleOptimizationInfo();
-      // The class staticizer lens shall not be applied through lens code rewriting or it breaks
-      // the lambda merger.
-      appView.clearCodeRewritings();
-    }
-
     // Commit synthetics before creating a builder (otherwise the builder will not include the
     // synthetics.)
     commitPendingSyntheticItemsR8(appView);
@@ -1474,12 +1440,6 @@
 
     previous = printMethod(code, "IR after argument type logging (SSA)", previous);
 
-    if (classStaticizer != null) {
-      timing.begin("Identify staticizing candidates");
-      classStaticizer.examineMethodCode(code);
-      timing.end();
-    }
-
     assert code.verifyTypes(appView);
 
     deadCodeRemover.run(code, timing);
@@ -1972,9 +1932,6 @@
     appView.withArgumentPropagator(argumentPropagator -> argumentPropagator.onMethodPruned(method));
     enumUnboxer.onMethodPruned(method);
     outliner.onMethodPruned(method);
-    if (classStaticizer != null) {
-      classStaticizer.onMethodPruned(method);
-    }
     if (inliner != null) {
       inliner.onMethodPruned(method);
     }
@@ -1992,9 +1949,6 @@
         argumentPropagator -> argumentPropagator.onMethodCodePruned(method));
     enumUnboxer.onMethodCodePruned(method);
     outliner.onMethodCodePruned(method);
-    if (classStaticizer != null) {
-      classStaticizer.onMethodCodePruned(method);
-    }
     if (inliner != null) {
       inliner.onMethodCodePruned(method);
     }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
index a52190d..8bcdea8 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
@@ -103,6 +103,7 @@
 import com.android.tools.r8.ir.optimize.enums.EnumUnboxer;
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.optimize.MemberRebindingAnalysis;
+import com.android.tools.r8.optimize.argumentpropagation.lenscoderewriter.NullCheckInserter;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.LazyBox;
 import com.android.tools.r8.verticalclassmerging.InterfaceTypeToClassTypeLensCodeRewriterHelper;
@@ -235,6 +236,8 @@
         new LazyBox<>(() -> new LensCodeRewriterUtils(appView, graphLens, codeLens));
     InterfaceTypeToClassTypeLensCodeRewriterHelper interfaceTypeToClassTypeRewriterHelper =
         InterfaceTypeToClassTypeLensCodeRewriterHelper.create(appView, code, graphLens, codeLens);
+    NullCheckInserter nullCheckInserter =
+        NullCheckInserter.create(appView, code, graphLens, codeLens);
     boolean mayHaveUnreachableBlocks = false;
     while (blocks.hasNext()) {
       BasicBlock block = blocks.next();
@@ -336,9 +339,6 @@
               Invoke.Type actualInvokeType = lensLookup.getType();
 
               iterator =
-                  insertNullCheckForInvokeReceiverIfNeeded(
-                      code, blocks, iterator, invoke, lensLookup);
-              iterator =
                   insertCastsForInvokeArgumentsIfNeeded(code, blocks, iterator, invoke, lensLookup);
 
               RewrittenPrototypeDescription prototypeChanges = lensLookup.getPrototypeChanges();
@@ -498,6 +498,9 @@
                 interfaceTypeToClassTypeRewriterHelper.insertCastsForOperandsIfNeeded(
                     invoke, newInvoke, lensLookup, blocks, block, iterator);
 
+                nullCheckInserter.insertNullCheckForInvokeReceiverIfNeeded(
+                    invoke, newInvoke, lensLookup);
+
                 if (newOutValue != null && newOutValue.getType() != current.getOutType()) {
                   affectedPhis.addAll(newOutValue.uniquePhiUsers());
                 }
@@ -785,10 +788,11 @@
       new DestructivePhiTypeUpdater(appView, graphLens, codeLens)
           .recomputeAndPropagateTypes(code, affectedPhis);
     }
+    nullCheckInserter.processWorklist();
     code.removeAllDeadAndTrivialPhis();
     removeUnusedArguments(method, code, unusedArguments);
 
-    // Finalize cast insertion.
+    // Finalize cast and null check insertion.
     interfaceTypeToClassTypeRewriterHelper.processWorklist();
 
     assert code.isConsistentSSABeforeTypesAreCorrect();
@@ -984,56 +988,6 @@
     return iterator;
   }
 
-  private InstructionListIterator insertNullCheckForInvokeReceiverIfNeeded(
-      IRCode code,
-      BasicBlockIterator blocks,
-      InstructionListIterator iterator,
-      InvokeMethod invoke,
-      MethodLookupResult lookup) {
-    // If the invoke has been staticized, then synthesize a null check for the receiver.
-    if (!invoke.isInvokeMethodWithReceiver()) {
-      return iterator;
-    }
-
-    ArgumentInfo receiverArgumentInfo =
-        lookup.getPrototypeChanges().getArgumentInfoCollection().getArgumentInfo(0);
-    if (!receiverArgumentInfo.isRemovedArgumentInfo()
-        || !receiverArgumentInfo.asRemovedArgumentInfo().isCheckNullOrZeroSet()) {
-      return iterator;
-    }
-
-    assert lookup.getType().isStatic();
-
-    TypeElement receiverType = invoke.asInvokeMethodWithReceiver().getReceiver().getType();
-    if (receiverType.isDefinitelyNotNull()) {
-      return iterator;
-    }
-
-    iterator.previous();
-
-    Position nullCheckPosition =
-        invoke
-            .getPosition()
-            .getOutermostCallerMatchingOrElse(
-                Position::isRemoveInnerFramesIfThrowingNpe, invoke.getPosition());
-    InvokeMethod nullCheck =
-        iterator.insertNullCheckInstruction(
-            appView, code, blocks, invoke.getFirstArgument(), nullCheckPosition);
-
-    // Reset the block iterator.
-    if (invoke.getBlock().hasCatchHandlers()) {
-      BasicBlock splitBlock = invoke.getBlock();
-      BasicBlock previousBlock = blocks.previousUntil(block -> block == splitBlock);
-      assert previousBlock == splitBlock;
-      blocks.next();
-      iterator = splitBlock.listIterator(code);
-    }
-
-    Instruction next = iterator.next();
-    assert next == invoke;
-    return iterator;
-  }
-
   private InstructionListIterator insertCastsForInvokeArgumentsIfNeeded(
       IRCode code,
       BasicBlockIterator blocks,
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriterUtils.java b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriterUtils.java
index 0702fc4..4adcf5f 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriterUtils.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriterUtils.java
@@ -236,8 +236,4 @@
     DexMethodHandle newHandle = rewriteDexMethodHandle(oldHandle, use, context);
     return newHandle != oldHandle ? new DexValueMethodHandle(newHandle) : methodHandle;
   }
-
-  public boolean hasGraphLens(GraphLens graphLens, GraphLens codeLens) {
-    return this.graphLens == graphLens && this.codeLens == codeLens;
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/MethodOptimizationFeedback.java b/src/main/java/com/android/tools/r8/ir/conversion/MethodOptimizationFeedback.java
index 8b229f9..0da54ff 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/MethodOptimizationFeedback.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/MethodOptimizationFeedback.java
@@ -54,8 +54,6 @@
 
   void markAsPropagated(DexEncodedMethod method);
 
-  void markCheckNullReceiverBeforeAnySideEffect(DexEncodedMethod method, boolean mark);
-
   void markTriggerClassInitBeforeAnySideEffect(DexEncodedMethod method, boolean mark);
 
   void setBridgeInfo(DexEncodedMethod method, BridgeInfo bridgeInfo);
@@ -87,8 +85,6 @@
 
   void unsetBridgeInfo(DexEncodedMethod method);
 
-  void unsetCheckNullReceiverBeforeAnySideEffect(ProgramMethod method);
-
   void unsetClassInitializerMayBePostponed(ProgramMethod method);
 
   void unsetClassInlinerMethodConstraint(ProgramMethod method);
@@ -131,7 +127,6 @@
     if (method.getOptimizationInfo().isMutableOptimizationInfo()) {
       unsetAbstractReturnValue(method);
       unsetBridgeInfo(method.getDefinition());
-      unsetCheckNullReceiverBeforeAnySideEffect(method);
       unsetClassInitializerMayBePostponed(method);
       unsetClassInlinerMethodConstraint(method);
       unsetDynamicReturnType(method);
@@ -158,8 +153,5 @@
     methodNeverReturnsNormally(method);
     setUnusedArguments(
         method, BitSetUtils.createFilled(true, method.getDefinition().getNumberOfArguments()));
-    if (method.getDefinition().isInstance()) {
-      markCheckNullReceiverBeforeAnySideEffect(method.getDefinition(), true);
-    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java b/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
index 69cc40b..33c69ac 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
@@ -91,8 +91,10 @@
     if (appView.options().enableBackportedMethodRewriting()) {
       backportedMethodRewriter = new BackportedMethodRewriter(appView);
     }
-    if (appView.options().apiModelingOptions().enableOutliningOfMethods) {
-      yieldingDesugarings.add(new ApiInvokeOutlinerDesugaring(appView, apiLevelCompute));
+    if (appView.options().apiModelingOptions().enableOutliningOfMethods
+        && appView.hasClassHierarchy()) {
+      yieldingDesugarings.add(
+          new ApiInvokeOutlinerDesugaring(appView.withClassHierarchy(), apiLevelCompute));
     }
     if (appView.options().enableTryWithResourcesDesugaring()) {
       desugarings.add(new TwrInstructionDesugaring(appView));
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/apimodel/ApiInvokeOutlinerDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/apimodel/ApiInvokeOutlinerDesugaring.java
index 8e2f9fb..a41ea13 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/apimodel/ApiInvokeOutlinerDesugaring.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/apimodel/ApiInvokeOutlinerDesugaring.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.cf.code.CfInvoke;
 import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
 import com.android.tools.r8.contexts.CompilationContext.UniqueContext;
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -20,6 +21,8 @@
 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.MethodResolutionResult;
+import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaring;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaringCollection;
@@ -38,10 +41,13 @@
  */
 public class ApiInvokeOutlinerDesugaring implements CfInstructionDesugaring {
 
-  private final AppView<?> appView;
+  private final AppView<? extends AppInfoWithClassHierarchy> appView;
   private final AndroidApiLevelCompute apiLevelCompute;
 
-  public ApiInvokeOutlinerDesugaring(AppView<?> appView, AndroidApiLevelCompute apiLevelCompute) {
+  public ApiInvokeOutlinerDesugaring(
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      AndroidApiLevelCompute apiLevelCompute) {
+    // TODO(b/216277460) We should only need AppView<?>.
     this.appView = appView;
     this.apiLevelCompute = apiLevelCompute;
   }
@@ -89,8 +95,8 @@
     if (!holderType.isClassType()) {
       return appView.computedMinApiLevel();
     }
-    DexClass clazz = appView.definitionFor(holderType);
-    if (clazz == null || !clazz.isLibraryClass()) {
+    DexClass holder = appView.definitionFor(holderType);
+    if (holder == null || !holder.isLibraryClass()) {
       return appView.computedMinApiLevel();
     }
     ComputedApiLevel methodApiLevel =
@@ -103,9 +109,21 @@
     // Compute the api level of the holder to see if the method will be stubbed.
     ComputedApiLevel holderApiLevel =
         apiLevelCompute.computeApiLevelForLibraryReference(holderType, ComputedApiLevel.unknown());
-    return methodApiLevel.isGreaterThan(holderApiLevel)
-        ? methodApiLevel
-        : appView.computedMinApiLevel();
+    if (holderApiLevel.isGreaterThanOrEqualTo(methodApiLevel)) {
+      return appView.computedMinApiLevel();
+    }
+    // Check for protected or package private access flags before outlining.
+    if (holder.isInterface()) {
+      return methodApiLevel;
+    } else {
+      MethodResolutionResult methodResolutionResult =
+          appView.appInfo().resolveMethod(cfInvoke.getMethod(), false);
+      SingleResolutionResult singleResolutionResult = methodResolutionResult.asSingleResolution();
+      return singleResolutionResult != null
+              && !singleResolutionResult.getResolvedMethod().isPublic()
+          ? methodApiLevel
+          : appView.computedMinApiLevel();
+    }
   }
 
   private boolean isApiLevelLessThanOrEqualTo9(ComputedApiLevel apiLevel) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
index 4e6bea5..a39688a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
@@ -374,15 +374,9 @@
       // receiver. Therefore, if the receiver may be null and the candidate inlinee does not
       // throw if the receiver is null before any other side effect, then we must synthesize a
       // null check.
-      if (!singleTarget
-          .getDefinition()
-          .getOptimizationInfo()
-          .checksNullReceiverBeforeAnySideEffect()) {
-        if (!inlinerOptions.enableInliningOfInvokesWithNullableReceivers) {
-          whyAreYouNotInliningReporter.reportReceiverMaybeNull();
-          return null;
-        }
-        action.setShouldSynthesizeNullCheckForReceiver();
+      if (!inlinerOptions.enableInliningOfInvokesWithNullableReceivers) {
+        whyAreYouNotInliningReporter.reportReceiverMaybeNull();
+        return null;
       }
     }
     return action;
@@ -484,9 +478,7 @@
       ProgramMethod singleTarget,
       InliningIRProvider inliningIRProvider,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
-    boolean removeInnerFramesIfThrowingNpe = false;
-    IRCode inlinee =
-        inliningIRProvider.getInliningIR(invoke, singleTarget, removeInnerFramesIfThrowingNpe);
+    IRCode inlinee = inliningIRProvider.getInliningIR(invoke, singleTarget);
 
     // In the Java VM Specification section "4.10.2.4. Instance Initialization Methods and
     // Newly Created Objects" it says:
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index 785c461..834e78d 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.ir.optimize;
 
 import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
+import static com.android.tools.r8.ir.optimize.SimpleDominatingEffectAnalysis.canInlineWithoutSynthesizingNullCheckForReceiver;
 import static com.android.tools.r8.utils.MapUtils.ignoreKey;
 import static com.google.common.base.Predicates.not;
 
@@ -51,6 +52,7 @@
 import com.android.tools.r8.ir.conversion.LensCodeRewriter;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.PostMethodProcessor;
+import com.android.tools.r8.ir.optimize.SimpleDominatingEffectAnalysis.SimpleEffectAnalysisResult;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackIgnore;
 import com.android.tools.r8.ir.optimize.inliner.DefaultInliningReasonStrategy;
@@ -515,7 +517,6 @@
     final Reason reason;
 
     private boolean shouldSynthesizeInitClass;
-    private boolean shouldSynthesizeNullCheckForReceiver;
 
     private DexProgramClass downcastClass;
 
@@ -539,15 +540,9 @@
     }
 
     void setShouldSynthesizeInitClass() {
-      assert !shouldSynthesizeNullCheckForReceiver;
       shouldSynthesizeInitClass = true;
     }
 
-    void setShouldSynthesizeNullCheckForReceiver() {
-      assert !shouldSynthesizeInitClass;
-      shouldSynthesizeNullCheckForReceiver = true;
-    }
-
     InlineeWithReason buildInliningIR(
         AppView<? extends AppInfoWithClassHierarchy> appView,
         InvokeMethod invoke,
@@ -558,12 +553,7 @@
       InternalOptions options = appView.options();
 
       // Build the IR for a yet not processed method, and perform minimal IR processing.
-      boolean removeInnerFramesIfThrowingNpe =
-          invoke.isInvokeMethodWithReceiver()
-              && invoke.asInvokeMethodWithReceiver().getReceiver().isMaybeNull()
-              && !shouldSynthesizeNullCheckForReceiver;
-      IRCode code =
-          inliningIRProvider.getInliningIR(invoke, target, removeInnerFramesIfThrowingNpe);
+      IRCode code = inliningIRProvider.getInliningIR(invoke, target);
 
       // Insert a init class instruction if this is needed to preserve class initialization
       // semantics.
@@ -582,11 +572,24 @@
           target.getDefinition().isSynchronized() && options.isGeneratingClassFiles();
       boolean isSynthesizingNullCheckForReceiverUsingMonitorEnter =
           shouldSynthesizeMonitorEnterExit && !target.getDefinition().isStatic();
-      if (shouldSynthesizeNullCheckForReceiver
+      if (invoke.isInvokeMethodWithReceiver()
+          && invoke.asInvokeMethodWithReceiver().getReceiver().isMaybeNull()
           && !isSynthesizingNullCheckForReceiverUsingMonitorEnter) {
-        synthesizeNullCheckForReceiver(appView, code, invoke);
+        SimpleEffectAnalysisResult checksReceiverBeingNull =
+            canInlineWithoutSynthesizingNullCheckForReceiver(appView, code);
+        if (!checksReceiverBeingNull.hasResult()
+            || !checksReceiverBeingNull.isPartial()
+            || (checksReceiverBeingNull.isPartial()
+                && checksReceiverBeingNull.topMostNotSatisfiedBlockSize() > 1)) {
+          synthesizeNullCheckForReceiver(appView, code, invoke, code.entryBlock());
+        } else {
+          checksReceiverBeingNull.forEachSatisfyingInstruction(
+              this::setRemoveInnerFramePositionForReceiverUse);
+          // Also add a null check on failing paths
+          checksReceiverBeingNull.forEachTopMostNotSatisfiedBlock(
+              block -> synthesizeNullCheckForReceiver(appView, code, invoke, block));
+        }
       }
-
       // Insert monitor-enter and monitor-exit instructions if the method is synchronized.
       if (shouldSynthesizeMonitorEnterExit) {
         TypeElement throwableType =
@@ -727,18 +730,15 @@
     }
 
     private void synthesizeNullCheckForReceiver(
-        AppView<?> appView, IRCode code, InvokeMethod invoke) {
+        AppView<?> appView, IRCode code, InvokeMethod invoke, BasicBlock block) {
       List<Value> arguments = code.collectArguments();
       if (!arguments.isEmpty()) {
         Value receiver = arguments.get(0);
         assert receiver.isThis();
-
-        BasicBlock entryBlock = code.entryBlock();
-
         // Insert a new block between the last argument instruction and the first actual
         // instruction of the method.
         BasicBlock throwBlock =
-            entryBlock.listIterator(code, arguments.size()).split(code, 0, null);
+            block.listIterator(code, block.isEntry() ? arguments.size() : 0).split(code, 0, null);
         assert !throwBlock.hasCatchHandlers();
 
         InstructionListIterator iterator = throwBlock.listIterator(code);
@@ -754,6 +754,21 @@
         assert false : "Unable to synthesize a null check for the receiver";
       }
     }
+
+    private void setRemoveInnerFramePositionForReceiverUse(Instruction instruction) {
+      Position position = instruction.getPosition();
+      if (position == null) {
+        assert false : "Expected position for inlinee call to receiver";
+        return;
+      }
+      Position removeInnerFrame =
+          position
+              .getOutermostCaller()
+              .builderWithCopy()
+              .setRemoveInnerFramesIfThrowingNpe(true)
+              .build();
+      instruction.forceOverwritePosition(position.replaceOutermostCallerPosition(removeInnerFrame));
+    }
   }
 
   public static class RetryAction extends InlineResult {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/SimpleDominatingEffectAnalysis.java b/src/main/java/com/android/tools/r8/ir/optimize/SimpleDominatingEffectAnalysis.java
new file mode 100644
index 0000000..8942395
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/SimpleDominatingEffectAnalysis.java
@@ -0,0 +1,324 @@
+// Copyright (c) 2022, 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;
+
+import static com.android.tools.r8.ir.optimize.SimpleDominatingEffectAnalysis.InstructionEffect.NO_EFFECT;
+import static com.android.tools.r8.ir.optimize.SimpleDominatingEffectAnalysis.InstructionEffect.OTHER_EFFECT;
+import static com.android.tools.r8.utils.TraversalContinuation.BREAK;
+import static com.android.tools.r8.utils.TraversalContinuation.CONTINUE;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.utils.DepthFirstSearchWorkListBase.DFSNodeWithState;
+import com.android.tools.r8.utils.DepthFirstSearchWorkListBase.StatefulDepthFirstSearchWorkList;
+import com.android.tools.r8.utils.IntBox;
+import com.android.tools.r8.utils.TraversalContinuation;
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+public class SimpleDominatingEffectAnalysis {
+
+  public enum InstructionEffect {
+    NO_EFFECT,
+    DESIRED_EFFECT,
+    OTHER_EFFECT;
+
+    public boolean isDesired() {
+      return this == DESIRED_EFFECT;
+    }
+
+    public boolean isOther() {
+      return this == OTHER_EFFECT;
+    }
+
+    public boolean isNoEffect() {
+      return this == NO_EFFECT;
+    }
+
+    public static InstructionEffect fromBoolean(boolean value) {
+      return value ? DESIRED_EFFECT : OTHER_EFFECT;
+    }
+
+    public ResultState toResultState() {
+      switch (this) {
+        case NO_EFFECT:
+          return ResultState.NOT_COMPUTED;
+        case DESIRED_EFFECT:
+          return ResultState.SATISFIED;
+        default:
+          assert isOther();
+          return ResultState.NOT_SATISFIED;
+      }
+    }
+  }
+
+  /**
+   * ResultState encodes a lattice on the following form: PARTIAL / \ SATISFIED NOT_SATISFIED \ /
+   * NOT_COMPUTED
+   *
+   * <p>PARTIAL results occur when have control flow where one or more branches are SATISFIED and
+   * one or more branches are NOT_SATISFIED.
+   */
+  private enum ResultState {
+    PARTIAL,
+    SATISFIED,
+    NOT_SATISFIED,
+    NOT_COMPUTED;
+
+    public boolean isPartial() {
+      return this == PARTIAL;
+    }
+
+    public boolean isSatisfied() {
+      return this == SATISFIED;
+    }
+
+    public boolean isNotSatisfied() {
+      return this == NOT_SATISFIED;
+    }
+
+    public boolean isNotComputed() {
+      return this == NOT_COMPUTED;
+    }
+
+    public ResultState join(ResultState other) {
+      if (isPartial() || other.isNotComputed()) {
+        return this;
+      }
+      if (isNotComputed() || other.isPartial()) {
+        return other;
+      }
+      if (this == other) {
+        return this;
+      }
+      return PARTIAL;
+    }
+  }
+
+  private static class ResultStateWithPartialBlocks {
+
+    private final ResultState state;
+    private final List<BasicBlock> failingBlocks;
+
+    private ResultStateWithPartialBlocks(ResultState state, List<BasicBlock> failingBlocks) {
+      this.state = state;
+      this.failingBlocks = failingBlocks;
+    }
+
+    public ResultStateWithPartialBlocks joinChildren(
+        List<DFSNodeWithState<BasicBlock, ResultStateWithPartialBlocks>> childNodes) {
+      assert state.isNotComputed();
+      ResultState newState =
+          childNodes.isEmpty() ? ResultState.NOT_SATISFIED : ResultState.NOT_COMPUTED;
+      for (DFSNodeWithState<BasicBlock, ResultStateWithPartialBlocks> childNode : childNodes) {
+        ResultStateWithPartialBlocks childState = childNode.getState();
+        assert !childState.state.isNotComputed();
+        newState = newState.join(childState.state);
+      }
+      assert !newState.isNotComputed();
+      List<BasicBlock> newFailingBlocks = new ArrayList<>();
+      if (newState.isPartial()) {
+        // Compute the initial basic blocks where that leads to OTHER_EFFECT.
+        for (DFSNodeWithState<BasicBlock, ResultStateWithPartialBlocks> childNode : childNodes) {
+          if (childNode.getState().state.isNotSatisfied()) {
+            newFailingBlocks.add(childNode.getNode());
+          } else if (childNode.getState().state.isPartial()) {
+            newFailingBlocks.addAll(childNode.getState().failingBlocks);
+          }
+        }
+      }
+      return new ResultStateWithPartialBlocks(newState, newFailingBlocks);
+    }
+  }
+
+  // The instruction analysis drives the SimpleDominatingEffectAnalysis by assigning an effect
+  // to a basic block.
+  public interface InstructionAnalysis {
+
+    // Analyse the instruction and assign and effect. If the desired effect is seen, it should
+    // dominate other users and DESIRED_EFFECT should be returned. If a violating effect is seen
+    // return OTHER_EFFECT. When an effect is seen there is no need to visit successor instructions
+    // in the block or successor blocks.
+    // If the instruction do not violate the effect, use NO_EFFECT. The analysis will, if the entire
+    // block is visited without any effect, visit successor blocks until an effect is found, the
+    // depth is violated or we see a return for which the result of this path will be notSatisfied.
+    InstructionEffect analyze(Instruction instruction);
+
+    // Return the successors of the block. The default is to only look at normal successors.
+    default List<BasicBlock> getSuccessors(BasicBlock block) {
+      return block.getNormalSuccessors();
+    }
+
+    // The max bound on instructions to consider before giving up.
+    default int maxNumberOfInstructions() {
+      return 100;
+    }
+  }
+
+  public static class SimpleEffectAnalysisResult {
+
+    private final List<Instruction> satisfyingInstructions;
+    private final List<BasicBlock> topmostNotSatisfiedBlocks;
+
+    private SimpleEffectAnalysisResult(
+        List<Instruction> satisfyingInstructions, List<BasicBlock> topmostNotSatisfiedBlocks) {
+
+      this.satisfyingInstructions = satisfyingInstructions;
+      this.topmostNotSatisfiedBlocks = topmostNotSatisfiedBlocks;
+    }
+
+    public boolean hasResult() {
+      return true;
+    }
+
+    public void forEachSatisfyingInstruction(Consumer<Instruction> instructionConsumer) {
+      satisfyingInstructions.forEach(instructionConsumer);
+    }
+
+    public void forEachTopMostNotSatisfiedBlock(Consumer<BasicBlock> blockConsumer) {
+      topmostNotSatisfiedBlocks.forEach(blockConsumer);
+    }
+
+    public int topMostNotSatisfiedBlockSize() {
+      return topmostNotSatisfiedBlocks.size();
+    }
+
+    public static SimpleEffectAnalysisResultBuilder builder() {
+      return new SimpleEffectAnalysisResultBuilder();
+    }
+
+    public boolean isPartial() {
+      return !satisfyingInstructions.isEmpty() && !topmostNotSatisfiedBlocks.isEmpty();
+    }
+  }
+
+  private static class SimpleEffectAnalysisResultBuilder {
+
+    List<Instruction> satisfyingInstructions = new ArrayList<>();
+    List<BasicBlock> failingBlocksForPartialResults = ImmutableList.of();
+    private boolean isFailed;
+
+    public void fail() {
+      isFailed = true;
+    }
+
+    public void addSatisfyingInstruction(Instruction instruction) {
+      satisfyingInstructions.add(instruction);
+    }
+
+    public void setFailingBlocksForPartialResults(List<BasicBlock> basicBlocks) {
+      this.failingBlocksForPartialResults = basicBlocks;
+    }
+
+    public SimpleEffectAnalysisResult build() {
+      return isFailed
+          ? NO_RESULT
+          : new SimpleEffectAnalysisResult(satisfyingInstructions, failingBlocksForPartialResults);
+    }
+  }
+
+  private static final SimpleEffectAnalysisResult NO_RESULT =
+      new SimpleEffectAnalysisResult(ImmutableList.of(), ImmutableList.of()) {
+        @Override
+        public boolean hasResult() {
+          return false;
+        }
+      };
+
+  public static SimpleEffectAnalysisResult run(IRCode code, InstructionAnalysis analysis) {
+    SimpleEffectAnalysisResultBuilder builder = SimpleEffectAnalysisResult.builder();
+    IntBox visitedInstructions = new IntBox();
+    new StatefulDepthFirstSearchWorkList<BasicBlock, ResultStateWithPartialBlocks>() {
+
+      @Override
+      protected TraversalContinuation process(
+          DFSNodeWithState<BasicBlock, ResultStateWithPartialBlocks> node,
+          Function<BasicBlock, DFSNodeWithState<BasicBlock, ResultStateWithPartialBlocks>>
+              childNodeConsumer) {
+        InstructionEffect effect = NO_EFFECT;
+        for (Instruction instruction : node.getNode().getInstructions()) {
+          if (visitedInstructions.getAndIncrement() > analysis.maxNumberOfInstructions()) {
+            builder.fail();
+            return BREAK;
+          }
+          effect = analysis.analyze(instruction);
+          if (!effect.isNoEffect()) {
+            if (effect.isDesired()) {
+              builder.addSatisfyingInstruction(instruction);
+            }
+            break;
+          }
+        }
+        if (effect.isNoEffect()) {
+          List<BasicBlock> successors = analysis.getSuccessors(node.getNode());
+          for (BasicBlock successor : successors) {
+            DFSNodeWithState<BasicBlock, ResultStateWithPartialBlocks> childNode =
+                childNodeConsumer.apply(successor);
+            if (childNode.hasState()) {
+              // If we see a block where the children have not been processed we cannot guarantee
+              // all paths having the effect since - ex. we could have a non-terminating loop.
+              builder.fail();
+              return BREAK;
+            }
+          }
+        }
+        node.setState(new ResultStateWithPartialBlocks(effect.toResultState(), ImmutableList.of()));
+        return CONTINUE;
+      }
+
+      @Override
+      protected TraversalContinuation joiner(
+          DFSNodeWithState<BasicBlock, ResultStateWithPartialBlocks> node,
+          List<DFSNodeWithState<BasicBlock, ResultStateWithPartialBlocks>> childNodes) {
+        ResultStateWithPartialBlocks resultState = node.getState();
+        if (resultState.state.isNotComputed()) {
+          resultState = resultState.joinChildren(childNodes);
+        } else {
+          assert resultState.state.isSatisfied() || resultState.state.isNotSatisfied();
+          assert childNodes.isEmpty();
+        }
+        node.setState(resultState);
+        if (node.getNode().isEntry()) {
+          builder.setFailingBlocksForPartialResults(resultState.failingBlocks);
+        }
+        return CONTINUE;
+      }
+    }.run(code.entryBlock());
+
+    return builder.build();
+  }
+
+  public static SimpleEffectAnalysisResult canInlineWithoutSynthesizingNullCheckForReceiver(
+      AppView<?> appView, IRCode code) {
+    assert code.context().getDefinition().isVirtualMethod();
+    Value receiver = code.getThis();
+    if (!receiver.isUsed()) {
+      return NO_RESULT;
+    }
+    ProgramMethod context = code.context();
+    return run(
+        code,
+        instruction -> {
+          if ((instruction.isInvokeMethodWithReceiver()
+                  && instruction.asInvokeMethodWithReceiver().getReceiver() == receiver)
+              || (instruction.isInstanceFieldInstruction()
+                  && instruction.asInstanceFieldInstruction().object() == receiver)
+              || (instruction.isMonitorEnter() && instruction.asMonitor().object() == receiver)) {
+            // Conservatively bailout if there are catch handlers.
+            return InstructionEffect.fromBoolean(!instruction.getBlock().hasCatchHandlers());
+          }
+          return instruction.instructionMayHaveSideEffects(appView, context)
+              ? OTHER_EFFECT
+              : NO_EFFECT;
+        });
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerCostAnalysis.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerCostAnalysis.java
index 06e2b18..93b5d8f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerCostAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerCostAnalysis.java
@@ -75,7 +75,7 @@
         // Not a direct inlinee.
         continue;
       }
-      IRCode inliningIR = inliningIRProvider.getAndCacheInliningIR(invoke, inlinee, false);
+      IRCode inliningIR = inliningIRProvider.getAndCacheInliningIR(invoke, inlinee);
       int increment =
           inlinee.getDefinition().getCode().estimatedSizeForInlining()
               - estimateSizeOfNonMaterializingInstructions(invoke, inliningIR);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
index 09b1cc3..95483ce 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
@@ -31,7 +31,6 @@
   static int UNKNOWN_RETURNED_ARGUMENT = -1;
   static boolean UNKNOWN_NEVER_RETURNS_NORMALLY = false;
   static AbstractValue UNKNOWN_ABSTRACT_RETURN_VALUE = UnknownValue.getInstance();
-  static boolean UNKNOWN_CHECKS_NULL_RECEIVER_BEFORE_ANY_SIDE_EFFECT = false;
   static boolean UNKNOWN_TRIGGERS_CLASS_INIT_BEFORE_ANY_SIDE_EFFECT = false;
   static boolean UNKNOWN_INITIALIZER_ENABLING_JAVA_ASSERTIONS = false;
   static boolean UNKNOWN_MAY_HAVE_SIDE_EFFECTS = true;
@@ -167,11 +166,6 @@
   }
 
   @Override
-  public boolean checksNullReceiverBeforeAnySideEffect() {
-    return UNKNOWN_CHECKS_NULL_RECEIVER_BEFORE_ANY_SIDE_EFFECT;
-  }
-
-  @Override
   public boolean triggersClassInitBeforeAnySideEffect() {
     return UNKNOWN_TRIGGERS_CLASS_INIT_BEFORE_ANY_SIDE_EFFECT;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
index a4667c9..17cffe5 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
@@ -88,8 +88,6 @@
 
   public abstract boolean forceInline();
 
-  public abstract boolean checksNullReceiverBeforeAnySideEffect();
-
   public abstract boolean triggersClassInitBeforeAnySideEffect();
 
   public abstract boolean mayHaveSideEffects();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
index a0c4eb2..acf0da6 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
@@ -542,11 +542,6 @@
       // Identifies if the method preserves class initialization after inlining.
       feedback.markTriggerClassInitBeforeAnySideEffect(
           method, triggersClassInitializationBeforeSideEffect(code));
-    } else {
-      // Identifies if the method preserves null check of the receiver after inlining.
-      Value receiver = code.getThis();
-      feedback.markCheckNullReceiverBeforeAnySideEffect(
-          method, receiver.isUsed() && checksNullBeforeSideEffect(code, receiver));
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
index 64bdb91..69f0497 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
@@ -89,7 +89,7 @@
   private static final int UNUSED_FLAG_1 = 0x20;
   private static final int NEVER_RETURNS_NORMALLY_FLAG = 0x40;
   private static final int UNUSED_FLAG_2 = 0x80;
-  private static final int CHECKS_NULL_RECEIVER_BEFORE_ANY_SIDE_EFFECT_FLAG = 0x100;
+  private static final int UNUSED_FLAG_3 = 0x100;
   private static final int TRIGGERS_CLASS_INIT_BEFORE_ANY_SIDE_EFFECT_FLAG = 0x200;
   private static final int INITIALIZER_ENABLING_JAVA_ASSERTIONS_FLAG = 0x400;
   private static final int REACHABILITY_SENSITIVE_FLAG = 0x800;
@@ -116,9 +116,7 @@
     defaultFlags |=
         BooleanUtils.intValue(defaultOptInfo.neverReturnsNormally()) * NEVER_RETURNS_NORMALLY_FLAG;
     defaultFlags |= 0 * UNUSED_FLAG_2;
-    defaultFlags |=
-        BooleanUtils.intValue(defaultOptInfo.checksNullReceiverBeforeAnySideEffect())
-            * CHECKS_NULL_RECEIVER_BEFORE_ANY_SIDE_EFFECT_FLAG;
+    defaultFlags |= 0 * UNUSED_FLAG_3;
     defaultFlags |=
         BooleanUtils.intValue(defaultOptInfo.triggersClassInitBeforeAnySideEffect())
             * TRIGGERS_CLASS_INIT_BEFORE_ANY_SIDE_EFFECT_FLAG;
@@ -488,11 +486,6 @@
   }
 
   @Override
-  public boolean checksNullReceiverBeforeAnySideEffect() {
-    return isFlagSet(CHECKS_NULL_RECEIVER_BEFORE_ANY_SIDE_EFFECT_FLAG);
-  }
-
-  @Override
   public boolean triggersClassInitBeforeAnySideEffect() {
     return isFlagSet(TRIGGERS_CLASS_INIT_BEFORE_ANY_SIDE_EFFECT_FLAG);
   }
@@ -671,14 +664,6 @@
     }
   }
 
-  void markCheckNullReceiverBeforeAnySideEffect(boolean mark) {
-    setFlag(CHECKS_NULL_RECEIVER_BEFORE_ANY_SIDE_EFFECT_FLAG, mark);
-  }
-
-  void unsetCheckNullReceiverBeforeAnySideEffect() {
-    clearFlag(CHECKS_NULL_RECEIVER_BEFORE_ANY_SIDE_EFFECT_FLAG);
-  }
-
   void markTriggerClassInitBeforeAnySideEffect(boolean mark) {
     setFlag(TRIGGERS_CLASS_INIT_BEFORE_ANY_SIDE_EFFECT_FLAG, mark);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java
index 5e9386c..4b6241a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java
@@ -229,12 +229,6 @@
   }
 
   @Override
-  public synchronized void markCheckNullReceiverBeforeAnySideEffect(
-      DexEncodedMethod method, boolean mark) {
-    getMethodOptimizationInfoForUpdating(method).markCheckNullReceiverBeforeAnySideEffect(mark);
-  }
-
-  @Override
   public synchronized void markTriggerClassInitBeforeAnySideEffect(
       DexEncodedMethod method, boolean mark) {
     getMethodOptimizationInfoForUpdating(method).markTriggerClassInitBeforeAnySideEffect(mark);
@@ -311,11 +305,6 @@
   }
 
   @Override
-  public synchronized void unsetCheckNullReceiverBeforeAnySideEffect(ProgramMethod method) {
-    getMethodOptimizationInfoForUpdating(method).unsetCheckNullReceiverBeforeAnySideEffect();
-  }
-
-  @Override
   public synchronized void unsetClassInitializerMayBePostponed(ProgramMethod method) {
     getMethodOptimizationInfoForUpdating(method).unsetClassInitializerMayBePostponed();
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java
index fd6d27e..ac66916 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java
@@ -94,9 +94,6 @@
   public void markProcessed(DexEncodedMethod method, ConstraintWithTarget state) {}
 
   @Override
-  public void markCheckNullReceiverBeforeAnySideEffect(DexEncodedMethod method, boolean mark) {}
-
-  @Override
   public void markTriggerClassInitBeforeAnySideEffect(DexEncodedMethod method, boolean mark) {}
 
   @Override
@@ -143,9 +140,6 @@
   public void unsetBridgeInfo(DexEncodedMethod method) {}
 
   @Override
-  public void unsetCheckNullReceiverBeforeAnySideEffect(ProgramMethod method) {}
-
-  @Override
   public void unsetClassInitializerMayBePostponed(ProgramMethod method) {}
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
index a763db6..5be58f0 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
@@ -137,11 +137,6 @@
   }
 
   @Override
-  public void markCheckNullReceiverBeforeAnySideEffect(DexEncodedMethod method, boolean mark) {
-    method.getMutableOptimizationInfo().markCheckNullReceiverBeforeAnySideEffect(mark);
-  }
-
-  @Override
   public void markTriggerClassInitBeforeAnySideEffect(DexEncodedMethod method, boolean mark) {
     // Ignored.
   }
@@ -234,12 +229,6 @@
   }
 
   @Override
-  public void unsetCheckNullReceiverBeforeAnySideEffect(ProgramMethod method) {
-    withMutableMethodOptimizationInfo(
-        method, MutableMethodOptimizationInfo::unsetCheckNullReceiverBeforeAnySideEffect);
-  }
-
-  @Override
   public void unsetClassInitializerMayBePostponed(ProgramMethod method) {
     withMutableMethodOptimizationInfo(
         method, MutableMethodOptimizationInfo::unsetClassInitializerMayBePostponed);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/InliningIRProvider.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/InliningIRProvider.java
index 677ccdd..958b02d 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/InliningIRProvider.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/InliningIRProvider.java
@@ -43,14 +43,12 @@
   public static InliningIRProvider getThrowingInstance() {
     return new InliningIRProvider() {
       @Override
-      public IRCode getInliningIR(
-          InvokeMethod invoke, ProgramMethod method, boolean removeInnerFramesIfNpe) {
+      public IRCode getInliningIR(InvokeMethod invoke, ProgramMethod method) {
         throw new Unreachable();
       }
 
       @Override
-      public IRCode getAndCacheInliningIR(
-          InvokeMethod invoke, ProgramMethod method, boolean removeInnerFrameIfThrowingNpe) {
+      public IRCode getAndCacheInliningIR(InvokeMethod invoke, ProgramMethod method) {
         throw new Unreachable();
       }
 
@@ -76,24 +74,23 @@
     };
   }
 
-  public IRCode getInliningIR(
-      InvokeMethod invoke, ProgramMethod method, boolean removeInnerFramesIfNpe) {
+  public IRCode getInliningIR(InvokeMethod invoke, ProgramMethod method) {
     IRCode cached = cache.remove(invoke);
     if (cached != null) {
       return cached;
     }
-    Position position = Position.getPositionForInlining(appView, invoke, context);
-    if (removeInnerFramesIfNpe) {
-      position = position.builderWithCopy().setRemoveInnerFramesIfThrowingNpe(true).build();
-    }
     Origin origin = method.getOrigin();
     return method.buildInliningIR(
-        context, appView, valueNumberGenerator, position, origin, methodProcessor);
+        context,
+        appView,
+        valueNumberGenerator,
+        Position.getPositionForInlining(appView, invoke, context),
+        origin,
+        methodProcessor);
   }
 
-  public IRCode getAndCacheInliningIR(
-      InvokeMethod invoke, ProgramMethod method, boolean removeInnerFrameIfThrowingNpe) {
-    IRCode inliningIR = getInliningIR(invoke, method, removeInnerFrameIfThrowingNpe);
+  public IRCode getAndCacheInliningIR(InvokeMethod invoke, ProgramMethod method) {
+    IRCode inliningIR = getInliningIR(invoke, method);
     cacheInliningIR(invoke, inliningIR);
     return inliningIR;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizer.java b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizer.java
deleted file mode 100644
index 30b0da1..0000000
--- a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizer.java
+++ /dev/null
@@ -1,827 +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.staticizer;
-
-import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
-
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexClassAndField;
-import com.android.tools.r8.graph.DexEncodedField;
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexField;
-import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.GraphLens;
-import com.android.tools.r8.graph.MethodResolutionResult;
-import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.graph.UseRegistry;
-import com.android.tools.r8.ir.code.IRCode;
-import com.android.tools.r8.ir.code.Instruction;
-import com.android.tools.r8.ir.code.InvokeDirect;
-import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
-import com.android.tools.r8.ir.code.InvokeStatic;
-import com.android.tools.r8.ir.code.NewInstance;
-import com.android.tools.r8.ir.code.StaticGet;
-import com.android.tools.r8.ir.code.StaticPut;
-import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.ir.conversion.IRConverter;
-import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
-import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.ListUtils;
-import com.android.tools.r8.utils.SetUtils;
-import com.android.tools.r8.utils.collections.LongLivedProgramMethodSetBuilder;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.ListIterator;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Predicate;
-
-public final class ClassStaticizer {
-
-  private final AppView<AppInfoWithLiveness> appView;
-  private final DexItemFactory factory;
-  private final IRConverter converter;
-
-  private GraphLens graphLensForOptimizationPass;
-
-  // Represents a staticizing candidate with all information
-  // needed for staticizing.
-  final class CandidateInfo {
-    final DexProgramClass candidate;
-    final DexEncodedField singletonField;
-    final AtomicBoolean preserveRead = new AtomicBoolean(false);
-    // Number of singleton field writes.
-    final AtomicInteger fieldWrites = new AtomicInteger();
-    // Number of instances created.
-    final AtomicInteger instancesCreated = new AtomicInteger();
-    final AtomicReference<DexEncodedMethod> constructor = new AtomicReference<>();
-    final AtomicReference<DexEncodedMethod> getter = new AtomicReference<>();
-
-    CandidateInfo(DexProgramClass candidate, DexEncodedField singletonField) {
-      assert candidate != null;
-      assert singletonField != null;
-      this.candidate = candidate;
-      this.singletonField = singletonField;
-
-      // register itself
-      candidates.put(candidate.type, this);
-    }
-
-    boolean isHostClassInitializer(ProgramMethod method) {
-      return method.getDefinition().isClassInitializer() && method.getHolderType() == hostType();
-    }
-
-    DexType hostType() {
-      return singletonField.getHolderType();
-    }
-
-    DexProgramClass hostClass() {
-      DexProgramClass hostClass = asProgramClassOrNull(appView.definitionFor(hostType()));
-      assert hostClass != null;
-      return hostClass;
-    }
-
-    CandidateInfo invalidate() {
-      candidates.remove(candidate.type);
-      return null;
-    }
-  }
-
-  final Map<CandidateInfo, LongLivedProgramMethodSetBuilder<?>> referencedFrom =
-      new ConcurrentHashMap<>();
-
-  private final Set<DexMethod> prunedMethods = Sets.newIdentityHashSet();
-
-  // The map storing all the potential candidates for staticizing.
-  final ConcurrentHashMap<DexType, CandidateInfo> candidates = new ConcurrentHashMap<>();
-
-  public ClassStaticizer(AppView<AppInfoWithLiveness> appView, IRConverter converter) {
-    this.appView = appView;
-    this.factory = appView.dexItemFactory();
-    this.converter = converter;
-  }
-
-  public void onMethodPruned(ProgramMethod method) {
-    onMethodCodePruned(method);
-  }
-
-  public void onMethodCodePruned(ProgramMethod method) {
-    prunedMethods.add(method.getReference());
-  }
-
-  public void prepareForPrimaryOptimizationPass(GraphLens graphLensForPrimaryOptimizationPass) {
-    collectCandidates();
-    this.graphLensForOptimizationPass = graphLensForPrimaryOptimizationPass;
-  }
-
-  public void prepareForSecondaryOptimizationPass(GraphLens graphLensForSecondaryOptimizationPass) {
-    // Rewrite all the referenced from sets such that they are all rewritten up until the lens of
-    // the second optimization pass. This is needed to ensure all elements in the referenced from
-    // sets are rewritten up until the same graph lens, in case any referenced from sets are
-    // extended during the secondary optimization pass.
-    assert appView.graphLens() == graphLensForSecondaryOptimizationPass;
-    referencedFrom
-        .values()
-        .forEach(
-            referencedFromBuilder ->
-                referencedFromBuilder
-                    .removeAll(prunedMethods)
-                    .rewrittenWithLens(graphLensForSecondaryOptimizationPass));
-    this.graphLensForOptimizationPass = graphLensForSecondaryOptimizationPass;
-    prunedMethods.clear();
-  }
-
-  // Before doing any usage-based analysis we collect a set of classes that can be
-  // candidates for staticizing. This analysis is very simple, but minimizes the
-  // set of eligible classes staticizer tracks and thus time and memory it needs.
-  public final void collectCandidates() {
-    Set<DexType> notEligible = Sets.newIdentityHashSet();
-    Map<DexType, DexEncodedField> singletonFields = new HashMap<>();
-
-    assert !appView.getSyntheticItems().hasPendingSyntheticClasses();
-    appView
-        .appInfo()
-        .classes()
-        .forEach(
-            cls -> {
-              // We only consider classes eligible for staticizing if there is just
-              // one single static field in the whole app which has a type of this
-              // class. This field will be considered to be a candidate for a singleton
-              // field. The requirements for the initialization of this field will be
-              // checked later.
-              for (DexEncodedField field : cls.staticFields()) {
-                DexType type = field.getReference().type;
-                if (singletonFields.put(type, field) != null) {
-                  // There is already candidate singleton field found.
-                  markNotEligible(type, notEligible);
-                }
-              }
-
-              // Don't allow fields with this candidate types.
-              for (DexEncodedField field : cls.instanceFields()) {
-                markNotEligible(field.getReference().type, notEligible);
-              }
-
-              // Don't allow methods that take a value of this type.
-              for (DexEncodedMethod method : cls.methods()) {
-                for (DexType parameter : method.getProto().parameters.values) {
-                  markNotEligible(parameter, notEligible);
-                }
-                if (method.isSynchronized()) {
-                  markNotEligible(cls.type, notEligible);
-                }
-              }
-
-              // High-level limitations on what classes we consider eligible.
-              if (cls.isInterface()
-                  // Must not be an interface or an abstract class.
-                  || cls.accessFlags.isAbstract()
-                  // Don't support candidates with instance fields
-                  || cls.instanceFields().size() > 0
-                  // Only support classes directly extending java.lang.Object
-                  || cls.superType != factory.objectType
-                  // The class must not have instantiated subtypes.
-                  || !cls.isEffectivelyFinal(appView)
-                  // Staticizing classes implementing interfaces is more
-                  // difficult, so don't support it until we really need it.
-                  || !cls.interfaces.isEmpty()) {
-                markNotEligible(cls.type, notEligible);
-              }
-            });
-
-    // Finalize the set of the candidates.
-    appView
-        .appInfo()
-        .classes()
-        .forEach(
-            cls -> {
-              DexType type = cls.type;
-              if (!notEligible.contains(type)) {
-                DexEncodedField field = singletonFields.get(type);
-                if (field != null
-                    && // Singleton field found
-                    !field.accessFlags.isVolatile()
-                    && // Don't remove volatile fields.
-                    !isPinned(cls, field)) { // Don't remove pinned objects.
-                  assert field.accessFlags.isStatic();
-                  // Note: we don't check that the field is final, since we will analyze
-                  //       later how and where it is initialized.
-                  new CandidateInfo(cls, field); // will self-register
-                }
-              }
-            });
-  }
-
-  private void markNotEligible(DexType type, Set<DexType> notEligible) {
-    if (type.isClassType()) {
-      notEligible.add(type);
-    }
-  }
-
-  private boolean isPinned(DexClass clazz, DexEncodedField singletonField) {
-    AppInfoWithLiveness appInfo = appView.appInfo();
-    if (appInfo.isPinned(clazz.type) || appInfo.isPinned(singletonField.getReference())) {
-      return true;
-    }
-    for (DexEncodedMethod method : clazz.methods()) {
-      if (!method.isStatic() && appInfo.isPinned(method.getReference())) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  // Check staticizing candidates' usages to ensure the candidate can be staticized.
-  //
-  // The criteria for type CANDIDATE to be eligible for staticizing fall into
-  // these categories:
-  //
-  //  * checking that there is only one instance of the class created, and it is created
-  //    inside the host class initializer, and it is guaranteed that nobody can access this
-  //    field before it is assigned.
-  //
-  //  * no other singleton field writes (except for those used to store the only candidate
-  //    class instance described above) are allowed.
-  //
-  //  * values read from singleton field should only be used for instance method calls.
-  //
-  // NOTE: there are more criteria eligible class needs to satisfy to be actually staticized,
-  // those will be checked later in staticizeCandidates().
-  //
-  // This method also collects all DexEncodedMethod instances that need to be rewritten if
-  // appropriate candidate is staticized. Essentially anything that references instance method
-  // or field defined in the class.
-  //
-  // NOTE: can be called concurrently.
-  public final void examineMethodCode(IRCode code) {
-    ProgramMethod context = code.context();
-    Set<Instruction> alreadyProcessed = Sets.newIdentityHashSet();
-
-    CandidateInfo receiverClassCandidateInfo = candidates.get(context.getHolderType());
-    Value receiverValue = code.getThis(); // NOTE: is null for static methods.
-    if (receiverClassCandidateInfo != null) {
-      if (receiverValue != null) {
-        // We are inside an instance method of candidate class (not an instance initializer
-        // which we will check later), check if all the references to 'this' are valid
-        // (the call will invalidate the candidate if some of them are not valid).
-        analyzeAllValueUsers(
-            receiverClassCandidateInfo,
-            receiverValue,
-            factory.isConstructor(context.getReference()));
-
-        // If the candidate is still valid, ignore all instructions
-        // we treat as valid usages on receiver.
-        if (candidates.get(context.getHolderType()) != null) {
-          alreadyProcessed.addAll(receiverValue.uniqueUsers());
-        }
-      } else {
-        // We are inside a static method of candidate class.
-        // Check if this is a valid getter of the singleton field.
-        if (context.getDefinition().returnType() == context.getHolderType()) {
-          List<Instruction> examined = isValidGetter(receiverClassCandidateInfo, code);
-          if (examined != null) {
-            DexEncodedMethod getter = receiverClassCandidateInfo.getter.get();
-            if (getter == null) {
-              receiverClassCandidateInfo.getter.set(context.getDefinition());
-              // Except for static-get and return, iterate other remaining instructions if any.
-              alreadyProcessed.addAll(examined);
-            } else {
-              assert getter != context.getDefinition();
-              // Not sure how to deal with many getters.
-              receiverClassCandidateInfo.invalidate();
-            }
-          } else {
-            // Invalidate the candidate if it has a static method whose return type is a candidate
-            // type but doesn't return the singleton field (in a trivial way).
-            receiverClassCandidateInfo.invalidate();
-          }
-        }
-      }
-    }
-
-    // TODO(b/143375203): if fully implemented, the following iterator could be:
-    //   InstructionListIterator iterator = code.instructionListIterator();
-    ListIterator<Instruction> iterator =
-        Lists.newArrayList(code.instructionIterator()).listIterator();
-    while (iterator.hasNext()) {
-      Instruction instruction = iterator.next();
-      if (alreadyProcessed.contains(instruction)) {
-        continue;
-      }
-
-      if (instruction.isNewInstance()) {
-        // Check the class being initialized against valid staticizing candidates.
-        NewInstance newInstance = instruction.asNewInstance();
-        CandidateInfo info = processInstantiation(context, iterator, newInstance);
-        if (info != null) {
-          alreadyProcessed.addAll(newInstance.outValue().aliasedUsers());
-          // For host class initializers having eligible instantiation we also want to
-          // ensure that the rest of the initializer consist of code w/o side effects.
-          // This must guarantee that removing field access will not result in missing side
-          // effects, otherwise we can still staticize, but cannot remove singleton reads.
-          while (iterator.hasNext()) {
-            if (!isAllowedInHostClassInitializer(context.getHolderType(), iterator.next(), code)) {
-              info.preserveRead.set(true);
-              iterator.previous();
-              break;
-            }
-            // Ignore just read instruction.
-          }
-          addReferencedFrom(info, context);
-        }
-        continue;
-      }
-
-      if (instruction.isStaticPut()) {
-        // Check the field being written to: no writes to singleton fields are allowed
-        // except for those processed in processInstantiation(...).
-        DexType candidateType = instruction.asStaticPut().getField().type;
-        CandidateInfo candidateInfo = candidates.get(candidateType);
-        if (candidateInfo != null) {
-          candidateInfo.invalidate();
-        }
-        continue;
-      }
-
-      if (instruction.isStaticGet()) {
-        // Check the field being read: make sure all usages are valid.
-        CandidateInfo info = processStaticFieldRead(instruction.asStaticGet());
-        if (info != null) {
-          addReferencedFrom(info, context);
-          // If the candidate is still valid, ignore all usages in further analysis.
-          Value value = instruction.outValue();
-          if (value != null) {
-            alreadyProcessed.addAll(value.aliasedUsers());
-          }
-        }
-        continue;
-      }
-
-      if (instruction.isInvokeStatic()) {
-        // Check if it is a static singleton getter.
-        CandidateInfo info = processInvokeStatic(instruction.asInvokeStatic());
-        if (info != null) {
-          addReferencedFrom(info, context);
-          // If the candidate is still valid, ignore all usages in further analysis.
-          Value value = instruction.outValue();
-          if (value != null) {
-            alreadyProcessed.addAll(value.aliasedUsers());
-          }
-        }
-        continue;
-      }
-
-      if (instruction.isInvokeMethodWithReceiver()) {
-        DexMethod invokedMethod = instruction.asInvokeMethodWithReceiver().getInvokedMethod();
-        CandidateInfo candidateInfo = candidates.get(invokedMethod.holder);
-        if (candidateInfo != null) {
-          // A call to instance method of the candidate class we don't know how to deal with.
-          candidateInfo.invalidate();
-        }
-        continue;
-      }
-
-      if (instruction.isInvokeCustom()) {
-        // Just invalidate any candidates referenced from non-static context.
-        CallSiteReferencesInvalidator invalidator =
-            new CallSiteReferencesInvalidator(appView, context);
-        invalidator.registerCallSite(instruction.asInvokeCustom().getCallSite());
-        continue;
-      }
-
-      if (instruction.isInstanceGet() || instruction.isInstancePut()) {
-        DexField fieldReferenced = instruction.asFieldInstruction().getField();
-        CandidateInfo candidateInfo = candidates.get(fieldReferenced.holder);
-        if (candidateInfo != null) {
-          // Reads/writes to instance field of the candidate class are not supported.
-          candidateInfo.invalidate();
-        }
-        continue;
-      }
-    }
-  }
-
-  private void addReferencedFrom(CandidateInfo info, ProgramMethod context) {
-    GraphLens currentGraphLens = appView.graphLens();
-    assert currentGraphLens == graphLensForOptimizationPass;
-    LongLivedProgramMethodSetBuilder<?> builder =
-        referencedFrom.computeIfAbsent(
-            info,
-            ignore ->
-                LongLivedProgramMethodSetBuilder.createConcurrentForIdentitySet(currentGraphLens));
-    builder.add(context);
-  }
-
-  private boolean isAllowedInHostClassInitializer(
-      DexType host, Instruction insn, IRCode code) {
-    return (insn.isStaticPut() && insn.asStaticPut().getField().holder == host) ||
-        insn.isConstNumber() ||
-        insn.isConstString() ||
-        (insn.isGoto() && insn.asGoto().isTrivialGotoToTheNextBlock(code)) ||
-        insn.isReturn();
-  }
-
-  private CandidateInfo processInstantiation(
-      ProgramMethod context, ListIterator<Instruction> iterator, NewInstance newInstance) {
-    DexType candidateType = newInstance.clazz;
-    CandidateInfo candidateInfo = candidates.get(candidateType);
-    if (candidateInfo == null) {
-      return null; // Not interested.
-    }
-
-    if (iterator.previousIndex() != 0) {
-      // Valid new instance must be the first instruction in the class initializer
-      return candidateInfo.invalidate();
-    }
-
-    if (!candidateInfo.isHostClassInitializer(context)) {
-      // A valid candidate must only have one instantiation which is
-      // done in the static initializer of the host class.
-      return candidateInfo.invalidate();
-    }
-
-    if (candidateInfo.instancesCreated.incrementAndGet() > 1) {
-      // Only one instance must be ever created.
-      return candidateInfo.invalidate();
-    }
-
-    Value candidateValue = newInstance.dest();
-    if (candidateValue == null) {
-      // Must be assigned to a singleton field.
-      return candidateInfo.invalidate();
-    }
-
-    if (candidateValue.numberOfPhiUsers() > 0) {
-      return candidateInfo.invalidate();
-    }
-
-    if (candidateValue.numberOfUsers() < 2) {
-      // We expect two special users for each instantiation: constructor call and static field
-      // write. We allow the instance to have other users as well, as long as they are valid
-      // according to the user analysis.
-      return candidateInfo.invalidate();
-    }
-
-    // Check usages. Currently we only support the patterns like:
-    //
-    //     static constructor void <clinit>() {
-    //        new-instance v0, <candidate-type>
-    //  (opt) const/4 v1, #int 0 // (optional)
-    //        invoke-direct {v0, ...}, void <candidate-type>.<init>(...)
-    //        sput-object v0, <instance-field>
-    //        ...
-    //        ... // other usages that are valid according to the user analysis.
-    //
-    // In case we guarantee candidate constructor does not access <instance-field>
-    // directly or indirectly we can guarantee that all the potential reads get
-    // same non-null value.
-
-    // Skip potential constant instructions
-    while (iterator.hasNext() && isNonThrowingConstInstruction(iterator.next())) {
-      // Intentionally empty.
-    }
-    iterator.previous();
-    if (!iterator.hasNext()) {
-      return candidateInfo.invalidate();
-    }
-    Set<Instruction> users = SetUtils.newIdentityHashSet(candidateValue.uniqueUsers());
-    Instruction constructorCall = iterator.next();
-    if (!isValidInitCall(candidateInfo, constructorCall, candidateValue, context)) {
-      iterator.previous();
-      return candidateInfo.invalidate();
-    }
-    boolean removedConstructorCall = users.remove(constructorCall);
-    assert removedConstructorCall;
-    if (!iterator.hasNext()) {
-      return candidateInfo.invalidate();
-    }
-    Instruction staticPut = iterator.next();
-    if (!isValidStaticPut(candidateInfo, staticPut)) {
-      iterator.previous();
-      return candidateInfo.invalidate();
-    }
-    boolean removedStaticPut = users.remove(staticPut);
-    assert removedStaticPut;
-    if (candidateInfo.fieldWrites.incrementAndGet() > 1) {
-      return candidateInfo.invalidate();
-    }
-    if (!isSelectedValueUsersValid(candidateInfo, candidateValue, false, users)) {
-      return candidateInfo.invalidate();
-    }
-    return candidateInfo;
-  }
-
-  private boolean isNonThrowingConstInstruction(Instruction instruction) {
-    return instruction.isConstInstruction() && !instruction.instructionTypeCanThrow();
-  }
-
-  private boolean isValidInitCall(
-      CandidateInfo info, Instruction instruction, Value candidateValue, ProgramMethod context) {
-    if (!instruction.isInvokeDirect()) {
-      return false;
-    }
-
-    // Check constructor.
-    InvokeDirect invoke = instruction.asInvokeDirect();
-    DexEncodedMethod methodInvoked =
-        appView.appInfo().lookupDirectTarget(invoke.getInvokedMethod(), context);
-    List<Value> values = invoke.inValues();
-
-    if (ListUtils.lastIndexMatching(values, v -> v.getAliasedValue() == candidateValue) != 0
-        || methodInvoked == null
-        || methodInvoked.getHolderType() != info.candidate.type) {
-      return false;
-    }
-
-    // Check arguments.
-    for (int i = 1; i < values.size(); i++) {
-      Value arg = values.get(i).getAliasedValue();
-      if (arg.isPhi() || !arg.definition.isConstInstruction()) {
-        return false;
-      }
-    }
-
-    DexEncodedMethod previous = info.constructor.getAndSet(methodInvoked);
-    assert previous == null;
-    return true;
-  }
-
-  private boolean isValidStaticPut(CandidateInfo info, Instruction instruction) {
-    if (!instruction.isStaticPut()) {
-      return false;
-    }
-    // Allow single assignment to a singleton field.
-    StaticPut staticPut = instruction.asStaticPut();
-    DexClassAndField fieldAccessed = appView.appInfo().lookupStaticTarget(staticPut.getField());
-    return fieldAccessed != null && fieldAccessed.getDefinition() == info.singletonField;
-  }
-
-  // Only allow a very trivial pattern: load the singleton field and return it, which looks like:
-  //
-  //   v <- static-get singleton-field
-  //   <assume instructions on v> // (optional)
-  //   return v // or aliased value
-  //
-  // Returns a list of instructions that are examined (as long as the method is a trivial getter).
-  private List<Instruction> isValidGetter(CandidateInfo info, IRCode code) {
-    List<Instruction> instructions = new ArrayList<>();
-    StaticGet staticGet = null;
-    for (Instruction instr : code.instructions()) {
-      if (instr.isStaticGet()) {
-        staticGet = instr.asStaticGet();
-        DexClassAndField fieldAccessed = appView.appInfo().lookupStaticTarget(staticGet.getField());
-        if (fieldAccessed == null || fieldAccessed.getDefinition() != info.singletonField) {
-          return null;
-        }
-        instructions.add(instr);
-        continue;
-      }
-      if (instr.isAssume() || instr.isReturn()) {
-        Value v = instr.inValues().get(0).getAliasedValue();
-        if (v.isPhi() || v.definition != staticGet) {
-          return null;
-        }
-        instructions.add(instr);
-        continue;
-      }
-      // All other instructions are not allowed.
-      return null;
-    }
-    return instructions;
-  }
-
-  // Static field get: can be a valid singleton field for a
-  // candidate in which case we should check if all the usages of the
-  // value read are eligible.
-  private CandidateInfo processStaticFieldRead(StaticGet staticGet) {
-    DexField field = staticGet.getField();
-    DexType candidateType = field.type;
-    CandidateInfo candidateInfo = candidates.get(candidateType);
-    if (candidateInfo == null) {
-      return null;
-    }
-
-    assert candidateInfo.singletonField
-            == appView.appInfo().lookupStaticTarget(field).getDefinition()
-        : "Added reference after collectCandidates(...)?";
-
-    Value singletonValue = staticGet.dest();
-    if (singletonValue != null) {
-      candidateInfo = analyzeAllValueUsers(candidateInfo, singletonValue, false);
-    }
-    return candidateInfo;
-  }
-
-  // Static getter: if this invokes a registered getter, treat it as static field get.
-  // That is, we should check if all the usages of the out value are eligible.
-  private CandidateInfo processInvokeStatic(InvokeStatic invoke) {
-    DexType candidateType = invoke.getInvokedMethod().proto.returnType;
-    CandidateInfo candidateInfo = candidates.get(candidateType);
-    if (candidateInfo == null) {
-      return null;
-    }
-
-    if (invoke.hasOutValue()
-        && candidateInfo.getter.get() != null
-        && candidateInfo.getter.get().getReference() == invoke.getInvokedMethod()) {
-      candidateInfo = analyzeAllValueUsers(candidateInfo, invoke.outValue(), false);
-    }
-    return candidateInfo;
-  }
-
-  private CandidateInfo analyzeAllValueUsers(
-      CandidateInfo candidateInfo, Value value, boolean ignoreSuperClassInitInvoke) {
-    assert value != null && value == value.getAliasedValue();
-    if (value.numberOfPhiUsers() > 0) {
-      return candidateInfo.invalidate();
-    }
-    if (!isSelectedValueUsersValid(
-        candidateInfo, value, ignoreSuperClassInitInvoke, value.uniqueUsers())) {
-      return candidateInfo.invalidate();
-    }
-    return candidateInfo;
-  }
-
-  private boolean isSelectedValueUsersValid(
-      CandidateInfo candidateInfo,
-      Value value,
-      boolean ignoreSuperClassInitInvoke,
-      Set<Instruction> currentUsers) {
-    while (!currentUsers.isEmpty()) {
-      Set<Instruction> indirectUsers = Sets.newIdentityHashSet();
-      for (Instruction user : currentUsers) {
-        if (!isValidValueUser(
-            candidateInfo, value, ignoreSuperClassInitInvoke, indirectUsers, user)) {
-          return false;
-        }
-      }
-      currentUsers = indirectUsers;
-    }
-    return true;
-  }
-
-  private boolean isValidValueUser(
-      CandidateInfo candidateInfo,
-      Value value,
-      boolean ignoreSuperClassInitInvoke,
-      Set<Instruction> indirectUsers,
-      Instruction user) {
-    if (user.isAssume()) {
-      if (user.outValue().numberOfPhiUsers() > 0) {
-        return false;
-      }
-      indirectUsers.addAll(user.outValue().uniqueUsers());
-      return true;
-    }
-    if (user.isInvokeVirtual() || user.isInvokeDirect() /* private methods */) {
-      InvokeMethodWithReceiver invoke = user.asInvokeMethodWithReceiver();
-      Predicate<Value> isAliasedValue = v -> v.getAliasedValue() == value;
-      DexMethod methodReferenced = invoke.getInvokedMethod();
-      if (factory.isConstructor(methodReferenced)) {
-        assert user.isInvokeDirect();
-        if (ignoreSuperClassInitInvoke
-            && ListUtils.lastIndexMatching(invoke.inValues(), isAliasedValue) == 0
-            && methodReferenced == factory.objectMembers.constructor) {
-          // If we are inside candidate constructor and analyzing usages
-          // of the receiver, we want to ignore invocations of superclass
-          // constructor which will be removed after staticizing.
-          return true;
-        }
-        return false;
-      }
-      AppInfoWithLiveness appInfo = appView.appInfo();
-      MethodResolutionResult resolutionResult =
-          appInfo.unsafeResolveMethodDueToDexFormat(methodReferenced);
-      DexEncodedMethod methodInvoked =
-          user.isInvokeDirect()
-              ? resolutionResult.lookupInvokeDirectTarget(candidateInfo.candidate, appInfo)
-              : resolutionResult.isVirtualTarget() ? resolutionResult.getSingleTarget() : null;
-      if (ListUtils.lastIndexMatching(invoke.inValues(), isAliasedValue) == 0
-          && methodInvoked != null
-          && methodInvoked.getHolderType() == candidateInfo.candidate.type) {
-        return true;
-      }
-    }
-
-    // All other users are not allowed.
-    return false;
-  }
-
-  // Perform staticizing candidates:
-  //
-  //  1. After filtering candidates based on usage, finalize the list of candidates by
-  //  filtering out candidates which don't satisfy the requirements:
-  //
-  //    * there must be one instance of the class
-  //    * constructor of the class used to create this instance must be a trivial one
-  //    * class initializer should only be present if candidate itself is own host
-  //    * no abstract or native instance methods
-  //
-  //  2. Rewrite instance methods of classes being staticized into static ones
-  //  3. Rewrite methods referencing staticized members, also remove instance creation
-  //
-  public final void staticizeCandidates(
-      OptimizationFeedback feedback, ExecutorService executorService) throws ExecutionException {
-    new StaticizingProcessor(appView, this, converter).run(feedback, executorService);
-  }
-
-  private class CallSiteReferencesInvalidator extends UseRegistry<ProgramMethod> {
-
-    CallSiteReferencesInvalidator(AppView<AppInfoWithLiveness> appView, ProgramMethod context) {
-      super(appView, context);
-    }
-
-    private void registerMethod(DexMethod method) {
-      registerTypeReference(method.holder);
-      registerProto(method.proto);
-    }
-
-    private void registerField(DexField field) {
-      registerTypeReference(field.holder);
-      registerTypeReference(field.type);
-    }
-
-    @Override
-    public void registerInitClass(DexType clazz) {
-      registerTypeReference(clazz);
-    }
-
-    @Override
-    public void registerInvokeVirtual(DexMethod method) {
-      registerMethod(method);
-    }
-
-    @Override
-    public void registerInvokeDirect(DexMethod method) {
-      registerMethod(method);
-    }
-
-    @Override
-    public void registerInvokeStatic(DexMethod method) {
-      registerMethod(method);
-    }
-
-    @Override
-    public void registerInvokeInterface(DexMethod method) {
-      registerMethod(method);
-    }
-
-    @Override
-    public void registerInvokeSuper(DexMethod method) {
-      registerMethod(method);
-    }
-
-    @Override
-    public void registerInstanceFieldWrite(DexField field) {
-      registerField(field);
-    }
-
-    @Override
-    public void registerInstanceFieldRead(DexField field) {
-      registerField(field);
-    }
-
-    @Override
-    public void registerNewInstance(DexType type) {
-      registerTypeReference(type);
-    }
-
-    @Override
-    public void registerStaticFieldRead(DexField field) {
-      registerField(field);
-    }
-
-    @Override
-    public void registerStaticFieldWrite(DexField field) {
-      registerField(field);
-    }
-
-    @Override
-    public void registerTypeReference(DexType type) {
-      CandidateInfo candidateInfo = candidates.get(type);
-      if (candidateInfo != null) {
-        candidateInfo.invalidate();
-      }
-    }
-
-    @Override
-    public void registerInstanceOf(DexType type) {
-      registerTypeReference(type);
-    }
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerGraphLens.java b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerGraphLens.java
deleted file mode 100644
index efc5228..0000000
--- a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerGraphLens.java
+++ /dev/null
@@ -1,31 +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.staticizer;
-
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexField;
-import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.NestedGraphLens;
-import com.android.tools.r8.ir.code.Invoke.Type;
-import com.android.tools.r8.utils.collections.BidirectionalOneToOneMap;
-
-class ClassStaticizerGraphLens extends NestedGraphLens {
-
-  ClassStaticizerGraphLens(
-      AppView<?> appView,
-      BidirectionalOneToOneMap<DexField, DexField> fieldMapping,
-      BidirectionalOneToOneMap<DexMethod, DexMethod> methodMapping) {
-    super(appView, fieldMapping, methodMapping, EMPTY_TYPE_MAP);
-  }
-
-  @Override
-  protected Type mapInvocationType(DexMethod newMethod, DexMethod originalMethod, Type type) {
-    if (methodMap.apply(originalMethod) == newMethod) {
-      assert type == Type.VIRTUAL || type == Type.DIRECT;
-      return Type.STATIC;
-    }
-    return super.mapInvocationType(newMethod, originalMethod, type);
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
deleted file mode 100644
index 62176cb..0000000
--- a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
+++ /dev/null
@@ -1,897 +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.staticizer;
-
-import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
-import static com.android.tools.r8.ir.analysis.type.Nullability.maybeNull;
-import static com.android.tools.r8.utils.PredicateUtils.not;
-
-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.DexEncodedField;
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexField;
-import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.graph.DexMember;
-import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider;
-import com.android.tools.r8.ir.analysis.type.TypeElement;
-import com.android.tools.r8.ir.code.IRCode;
-import com.android.tools.r8.ir.code.Instruction;
-import com.android.tools.r8.ir.code.InstructionListIterator;
-import com.android.tools.r8.ir.code.InvokeDirect;
-import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
-import com.android.tools.r8.ir.code.InvokeStatic;
-import com.android.tools.r8.ir.code.NewInstance;
-import com.android.tools.r8.ir.code.Phi;
-import com.android.tools.r8.ir.code.StaticGet;
-import com.android.tools.r8.ir.code.StaticPut;
-import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.ir.conversion.IRConverter;
-import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
-import com.android.tools.r8.ir.conversion.MethodProcessor;
-import com.android.tools.r8.ir.conversion.OneTimeMethodProcessor;
-import com.android.tools.r8.ir.optimize.AssumeInserter;
-import com.android.tools.r8.ir.optimize.ClassInitializerDefaultsOptimization.ClassInitializerDefaultsResult;
-import com.android.tools.r8.ir.optimize.CodeRewriter;
-import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
-import com.android.tools.r8.ir.optimize.staticizer.ClassStaticizer.CandidateInfo;
-import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.SetUtils;
-import com.android.tools.r8.utils.Timing;
-import com.android.tools.r8.utils.TraversalContinuation;
-import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
-import com.android.tools.r8.utils.collections.LongLivedProgramMethodSetBuilder;
-import com.android.tools.r8.utils.collections.MutableBidirectionalOneToOneMap;
-import com.android.tools.r8.utils.collections.ProgramMethodSet;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Sets;
-import com.google.common.collect.Streams;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.IdentityHashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.function.BiConsumer;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-
-// TODO(b/140766440): Use PostProcessor, instead of having its own post processing.
-final class StaticizingProcessor {
-
-  private final AppView<AppInfoWithLiveness> appView;
-  private final ClassStaticizer classStaticizer;
-  private final IRConverter converter;
-
-  private final ProgramMethodSet methodsToReprocess = ProgramMethodSet.create();
-
-  // Optimization order matters, hence a collection that preserves orderings.
-  private final Map<DexEncodedMethod, ImmutableList.Builder<BiConsumer<IRCode, MethodProcessor>>>
-      processingQueue = new IdentityHashMap<>();
-
-  private final ProgramMethodSet referencingExtraMethods = ProgramMethodSet.create();
-  private final Map<DexEncodedMethod, CandidateInfo> hostClassInits = new IdentityHashMap<>();
-  private final ProgramMethodSet methodsToBeStaticized = ProgramMethodSet.create();
-  private final Map<DexField, CandidateInfo> singletonFields = new IdentityHashMap<>();
-  private final Map<DexMethod, CandidateInfo> singletonGetters = new IdentityHashMap<>();
-  private final Map<DexType, DexType> candidateToHostMapping = new IdentityHashMap<>();
-
-  StaticizingProcessor(
-      AppView<AppInfoWithLiveness> appView,
-      ClassStaticizer classStaticizer,
-      IRConverter converter) {
-    this.appView = appView;
-    this.classStaticizer = classStaticizer;
-    this.converter = converter;
-  }
-
-  final void run(OptimizationFeedback feedback, ExecutorService executorService)
-      throws ExecutionException {
-    // Filter out candidates based on the information we collected while examining methods.
-    Map<CandidateInfo, ProgramMethodSet> materializedReferencedFromCollections =
-        finalEligibilityCheck();
-
-    // Prepare interim data.
-    prepareCandidates(materializedReferencedFromCollections);
-
-    // Enqueue all host class initializers (only remove instantiations).
-    ProgramMethodSet hostClassInitMethods = ProgramMethodSet.create();
-    hostClassInits
-        .values()
-        .forEach(
-            candidateInfo ->
-                hostClassInitMethods.add(candidateInfo.hostClass().getProgramClassInitializer()));
-    enqueueMethodsWithCodeOptimizations(
-        hostClassInitMethods,
-        optimizations ->
-            optimizations
-                .add(this::removeCandidateInstantiation)
-                .add(this::insertAssumeInstructions)
-                .add(collectOptimizationInfo(feedback)));
-
-    // Enqueue instance methods to be staticized (only remove references to 'this'). Intentionally
-    // not collecting optimization info for these methods, since they will be reprocessed again
-    // below once staticized.
-    enqueueMethodsWithCodeOptimizations(
-        methodsToBeStaticized, optimizations -> optimizations.add(this::removeReferencesToThis));
-
-    // Rewrite outliner with lens.
-    converter.outliner.rewriteWithLens();
-
-    // Process queued methods with associated optimizations
-    processMethodsConcurrently(feedback, executorService);
-
-    // TODO(b/140767158): Merge the remaining part below.
-    // Convert instance methods into static methods with an extra parameter.
-    ProgramMethodSet methods = staticizeMethodSymbols();
-
-    // Process all other methods that may reference singleton fields and call methods on them.
-    // (Note that we exclude the former instance methods, but include new static methods created as
-    // a result of staticizing.)
-    methods.addAll(referencingExtraMethods);
-    methods.addAll(hostClassInitMethods);
-    enqueueMethodsWithCodeOptimizations(
-        methods,
-        optimizations ->
-            optimizations
-                .add(this::rewriteReferences)
-                .add(this::insertAssumeInstructions)
-                .add(collectOptimizationInfo(feedback)));
-
-    // Rewrite outliner with lens.
-    converter.outliner.rewriteWithLens();
-
-    // Process queued methods with associated optimizations
-    processMethodsConcurrently(feedback, executorService);
-
-    // Clear all candidate information now that all candidates have been staticized.
-    classStaticizer.candidates.clear();
-  }
-
-  private Map<CandidateInfo, ProgramMethodSet> finalEligibilityCheck() {
-    Map<CandidateInfo, ProgramMethodSet> materializedReferencedFromCollections =
-        new IdentityHashMap<>();
-    Set<Phi> visited = Sets.newIdentityHashSet();
-    Set<Phi> trivialPhis = Sets.newIdentityHashSet();
-    Iterator<Entry<DexType, CandidateInfo>> it = classStaticizer.candidates.entrySet().iterator();
-    while (it.hasNext()) {
-      Entry<DexType, CandidateInfo> entry = it.next();
-      DexType candidateType = entry.getKey();
-      CandidateInfo info = entry.getValue();
-      DexProgramClass candidateClass = info.candidate;
-      DexType candidateHostType = info.hostType();
-      DexEncodedMethod constructorUsed = info.constructor.get();
-
-      int instancesCreated = info.instancesCreated.get();
-      assert instancesCreated == info.fieldWrites.get();
-      assert instancesCreated <= 1;
-      assert (instancesCreated == 0) == (constructorUsed == null);
-
-      // CHECK: One instance, one singleton field, known constructor
-      if (instancesCreated == 0) {
-        // Give up on the candidate, if there are any reads from instance
-        // field the user should read null.
-        it.remove();
-        continue;
-      }
-
-      // CHECK: instance initializer used to create an instance is trivial.
-      // NOTE: Along with requirement that candidate does not have instance
-      // fields this should guarantee that the constructor is empty.
-      assert candidateClass.instanceFields().size() == 0;
-      assert constructorUsed.isProcessed();
-      if (constructorUsed.getOptimizationInfo().mayHaveSideEffects()) {
-        it.remove();
-        continue;
-      }
-
-      // CHECK: class initializer should only be present if candidate itself is its own host.
-      DexEncodedMethod classInitializer = candidateClass.getClassInitializer();
-      assert classInitializer != null || candidateType != candidateHostType;
-      if (classInitializer != null && candidateType != candidateHostType) {
-        it.remove();
-        continue;
-      }
-
-      // CHECK: no abstract or native instance methods.
-      if (Streams.stream(candidateClass.methods()).anyMatch(
-          method -> !method.isStatic() && (method.shouldNotHaveCode()))) {
-        it.remove();
-        continue;
-      }
-
-      // CHECK: references to 'this' in instance methods are fixable.
-      TraversalContinuation fixableThisPointer =
-          candidateClass.traverseProgramMethods(
-              method -> {
-                IRCode code = method.buildIR(appView);
-                assert code != null;
-                Value thisValue = code.getThis();
-                assert thisValue != null;
-                visited.clear();
-                trivialPhis.clear();
-                boolean onlyHasTrivialPhis =
-                    testAndCollectPhisComposedOfThis(
-                        visited, thisValue.uniquePhiUsers(), thisValue, trivialPhis);
-                if (thisValue.hasPhiUsers() && !onlyHasTrivialPhis) {
-                  return TraversalContinuation.BREAK;
-                }
-                return TraversalContinuation.CONTINUE;
-              },
-              definition -> !definition.isStatic() && !definition.isInstanceInitializer());
-      if (fixableThisPointer.shouldBreak()) {
-        it.remove();
-        continue;
-      }
-
-      ProgramMethodSet referencedFrom;
-      if (classStaticizer.referencedFrom.containsKey(info)) {
-        LongLivedProgramMethodSetBuilder<?> referencedFromBuilder =
-            classStaticizer.referencedFrom.remove(info);
-        assert referencedFromBuilder != null;
-        referencedFrom =
-            referencedFromBuilder
-                .rewrittenWithLens(appView)
-                .build(appView);
-        materializedReferencedFromCollections.put(info, referencedFrom);
-      } else {
-        referencedFrom = ProgramMethodSet.empty();
-      }
-
-      // CHECK: references to field read usages are fixable.
-      boolean fixableFieldReads = true;
-      for (ProgramMethod method : referencedFrom) {
-        IRCode code = method.buildIR(appView);
-        assert code != null;
-        List<Instruction> singletonUsers =
-            Streams.stream(code.instructionIterator())
-                .filter(
-                    instruction -> {
-                      if (instruction.isStaticGet()
-                          && instruction.asStaticGet().getField()
-                              == info.singletonField.getReference()) {
-                        return true;
-                      }
-                      DexEncodedMethod getter = info.getter.get();
-                      return getter != null
-                          && instruction.isInvokeStatic()
-                          && instruction.asInvokeStatic().getInvokedMethod()
-                              == getter.getReference();
-                    })
-                .collect(Collectors.toList());
-        boolean fixableFieldReadsPerUsage = true;
-        for (Instruction user : singletonUsers) {
-          if (user.outValue() == null) {
-            continue;
-          }
-          Value dest = user.outValue();
-          visited.clear();
-          trivialPhis.clear();
-          assert user.isInvokeStatic() || user.isStaticGet();
-          DexMember member =
-              user.isStaticGet()
-                  ? user.asStaticGet().getField()
-                  : user.asInvokeStatic().getInvokedMethod();
-          boolean onlyHasTrivialPhis =
-              testAndCollectPhisComposedOfSameMember(
-                  visited, dest.uniquePhiUsers(), member, trivialPhis);
-          if (dest.hasPhiUsers() && !onlyHasTrivialPhis) {
-            fixableFieldReadsPerUsage = false;
-            break;
-          }
-        }
-        if (!fixableFieldReadsPerUsage) {
-          fixableFieldReads = false;
-          break;
-        }
-      }
-      if (!fixableFieldReads) {
-        it.remove();
-        continue;
-      }
-    }
-    return materializedReferencedFromCollections;
-  }
-
-  private void prepareCandidates(
-      Map<CandidateInfo, ProgramMethodSet> materializedReferencedFromCollections) {
-    Set<DexEncodedMethod> removedInstanceMethods = Sets.newIdentityHashSet();
-
-    for (CandidateInfo candidate : classStaticizer.candidates.values()) {
-      DexProgramClass candidateClass = candidate.candidate;
-      // Host class initializer
-      DexClass hostClass = candidate.hostClass();
-      DexEncodedMethod hostClassInitializer = hostClass.getClassInitializer();
-      assert hostClassInitializer != null;
-      CandidateInfo previous = hostClassInits.put(hostClassInitializer, candidate);
-      assert previous == null;
-
-      // Collect instance methods to be staticized.
-      candidateClass.forEachProgramMethodMatching(
-          definition -> {
-            if (!definition.isStatic()) {
-              removedInstanceMethods.add(definition);
-              return !definition.isInstanceInitializer();
-            }
-            return false;
-          },
-          methodsToBeStaticized::add);
-      singletonFields.put(candidate.singletonField.getReference(), candidate);
-      DexEncodedMethod getter = candidate.getter.get();
-      if (getter != null) {
-        singletonGetters.put(getter.getReference(), candidate);
-      }
-      ProgramMethodSet referencedFrom =
-          materializedReferencedFromCollections.getOrDefault(candidate, ProgramMethodSet.empty());
-      assert validMethods(referencedFrom);
-      referencingExtraMethods.addAll(referencedFrom);
-    }
-
-    removedInstanceMethods.forEach(referencingExtraMethods::remove);
-  }
-
-  private boolean validMethods(ProgramMethodSet referencedFrom) {
-    for (ProgramMethod method : referencedFrom) {
-      DexClass clazz = appView.definitionForHolder(method.getReference());
-      assert clazz != null;
-      assert clazz.lookupMethod(method.getReference()) == method.getDefinition();
-    }
-    return true;
-  }
-
-  private void enqueueMethodsWithCodeOptimizations(
-      Iterable<ProgramMethod> methods,
-      Consumer<ImmutableList.Builder<BiConsumer<IRCode, MethodProcessor>>> extension) {
-    for (ProgramMethod method : methods) {
-      methodsToReprocess.add(method);
-      extension.accept(
-          processingQueue.computeIfAbsent(
-              method.getDefinition(), ignore -> ImmutableList.builder()));
-    }
-  }
-
-  /**
-   * Processes the given methods concurrently using the given strategy.
-   *
-   * <p>Note that, when the strategy {@link #rewriteReferences(IRCode, MethodProcessor)} is being
-   * applied, it is important that we never inline a method from `methods` which has still not been
-   * reprocessed. This could lead to broken code, because the strategy that rewrites the broken
-   * references is applied *before* inlining (because the broken references in the inlinee are never
-   * rewritten). We currently avoid this situation by processing all the methods concurrently
-   * (inlining of a method that is processed concurrently is not allowed).
-   */
-  private void processMethodsConcurrently(
-      OptimizationFeedback feedback, ExecutorService executorService) throws ExecutionException {
-    OneTimeMethodProcessor methodProcessor =
-        OneTimeMethodProcessor.create(methodsToReprocess, appView);
-    methodProcessor.forEachWaveWithExtension(
-        (method, methodProcessingContext) ->
-            forEachMethod(
-                method,
-                processingQueue.get(method.getDefinition()).build(),
-                feedback,
-                methodProcessor),
-        executorService);
-    // TODO(b/140767158): No need to clear if we can do every thing in one go.
-    methodsToReprocess.clear();
-    processingQueue.clear();
-  }
-
-  // TODO(b/140766440): Should be part or variant of PostProcessor.
-  private void forEachMethod(
-      ProgramMethod method,
-      Collection<BiConsumer<IRCode, MethodProcessor>> codeOptimizations,
-      OptimizationFeedback feedback,
-      OneTimeMethodProcessor methodProcessor) {
-    IRCode code = method.buildIR(appView);
-    codeOptimizations.forEach(codeOptimization -> codeOptimization.accept(code, methodProcessor));
-    CodeRewriter.removeAssumeInstructions(appView, code);
-    converter.removeDeadCodeAndFinalizeIR(code, feedback, Timing.empty());
-  }
-
-  private void insertAssumeInstructions(IRCode code, MethodProcessor methodProcessor) {
-    AssumeInserter assumeInserter = converter.assumeInserter;
-    if (assumeInserter != null) {
-      assumeInserter.insertAssumeInstructions(code, Timing.empty());
-    }
-  }
-
-  private BiConsumer<IRCode, MethodProcessor> collectOptimizationInfo(
-      OptimizationFeedback feedback) {
-    return (code, methodProcessor) ->
-        converter.collectOptimizationInfo(
-            code.context(),
-            code,
-            ClassInitializerDefaultsResult.empty(),
-            feedback,
-            methodProcessor,
-            new MutableMethodConversionOptions(methodProcessor),
-            BytecodeMetadataProvider.builder(),
-            Timing.empty());
-  }
-
-  private void removeCandidateInstantiation(IRCode code, MethodProcessor methodProcessor) {
-    CandidateInfo candidateInfo = hostClassInits.get(code.method());
-    assert candidateInfo != null;
-
-    // Find and remove instantiation and its users.
-    for (NewInstance newInstance : code.<NewInstance>instructions(Instruction::isNewInstance)) {
-      if (newInstance.clazz == candidateInfo.candidate.type) {
-        // Remove all usages
-        // NOTE: requiring (a) the instance initializer to be trivial, (b) not allowing
-        //       candidates with instance fields and (c) requiring candidate to directly
-        //       extend java.lang.Object guarantees that the constructor is actually
-        //       empty and does not need to be inlined.
-        assert candidateInfo.candidate.superType == factory().objectType;
-        assert candidateInfo.candidate.instanceFields().size() == 0;
-
-        Value singletonValue = newInstance.outValue();
-        assert singletonValue != null;
-
-        InvokeDirect uniqueConstructorInvoke =
-            newInstance.getUniqueConstructorInvoke(appView.dexItemFactory());
-        assert uniqueConstructorInvoke != null;
-        uniqueConstructorInvoke.removeOrReplaceByDebugLocalRead(code);
-
-        StaticPut uniqueStaticPut = null;
-        for (Instruction user : singletonValue.uniqueUsers()) {
-          if (user.isStaticPut()) {
-            assert uniqueStaticPut == null;
-            uniqueStaticPut = user.asStaticPut();
-          }
-        }
-        assert uniqueStaticPut != null;
-        uniqueStaticPut.removeOrReplaceByDebugLocalRead(code);
-
-        if (newInstance.outValue().hasAnyUsers()) {
-          TypeElement type = TypeElement.fromDexType(newInstance.clazz, maybeNull(), appView);
-          newInstance.replace(
-              new StaticGet(code.createValue(type), candidateInfo.singletonField.getReference()),
-              code);
-        } else {
-          newInstance.removeOrReplaceByDebugLocalRead(code);
-        }
-        return;
-      }
-    }
-
-    assert candidateInfo.singletonField.getOptimizationInfo().isDead()
-        : "Must always be able to find and remove the instantiation";
-  }
-
-  private void removeReferencesToThis(IRCode code, MethodProcessor methodProcessor) {
-    fixupStaticizedThisUsers(code, code.getThis());
-  }
-
-  private void rewriteReferences(IRCode code, MethodProcessor methodProcessor) {
-    // Fetch all instructions that reference singletons to avoid concurrent modifications to the
-    // instruction list that can arise from doing it directly in the iterator.
-    List<Instruction> singletonUsers =
-        Streams.stream(code.instructionIterator())
-            .filter(
-                instruction ->
-                    (instruction.isStaticGet()
-                            && singletonFields.containsKey(
-                                instruction.asFieldInstruction().getField()))
-                        || (instruction.isInvokeStatic()
-                            && singletonGetters.containsKey(
-                                instruction.asInvokeStatic().getInvokedMethod())))
-            .collect(Collectors.toList());
-    for (Instruction singletonUser : singletonUsers) {
-      CandidateInfo candidateInfo;
-      DexMember member;
-      if (singletonUser.isStaticGet()) {
-        candidateInfo = singletonFields.get(singletonUser.asStaticGet().getField());
-        member = singletonUser.asStaticGet().getField();
-      } else {
-        assert singletonUser.isInvokeStatic();
-        candidateInfo = singletonGetters.get(singletonUser.asInvokeStatic().getInvokedMethod());
-        member = singletonUser.asInvokeStatic().getInvokedMethod();
-      }
-      Value value = singletonUser.outValue();
-      if (value != null) {
-        fixupStaticizedFieldUsers(code, value, member);
-      }
-      if (!candidateInfo.preserveRead.get()) {
-        singletonUser.removeOrReplaceByDebugLocalRead(code);
-      }
-    }
-    if (!candidateToHostMapping.isEmpty()) {
-      remapMovedCandidates(code);
-    }
-  }
-
-  private boolean testAndCollectPhisComposedOfThis(
-      Set<Phi> visited, Set<Phi> phisToCheck, Value thisValue, Set<Phi> trivialPhis) {
-    for (Phi phi : phisToCheck) {
-      if (!visited.add(phi)) {
-        continue;
-      }
-      Set<Phi> chainedPhis = Sets.newIdentityHashSet();
-      for (Value operand : phi.getOperands()) {
-        Value v = operand.getAliasedValue();
-        if (v.isPhi()) {
-          chainedPhis.add(operand.asPhi());
-        } else {
-          if (v != thisValue) {
-            return false;
-          }
-        }
-      }
-      if (!chainedPhis.isEmpty()) {
-        if (!testAndCollectPhisComposedOfThis(visited, chainedPhis, thisValue, trivialPhis)) {
-          return false;
-        }
-      }
-      trivialPhis.add(phi);
-    }
-    return true;
-  }
-
-  // Fixup `this` usages: rewrites all method calls so that they point to static methods.
-  private void fixupStaticizedThisUsers(IRCode code, Value thisValue) {
-    assert thisValue != null && thisValue == thisValue.getAliasedValue();
-    // Depending on other optimizations, e.g., inlining, `this` can be flown to phis.
-    Set<Phi> trivialPhis = Sets.newIdentityHashSet();
-    boolean onlyHasTrivialPhis = testAndCollectPhisComposedOfThis(
-        Sets.newIdentityHashSet(), thisValue.uniquePhiUsers(), thisValue, trivialPhis);
-    assert !thisValue.hasPhiUsers() || onlyHasTrivialPhis;
-    assert trivialPhis.isEmpty() || onlyHasTrivialPhis;
-
-    Set<Instruction> users = SetUtils.newIdentityHashSet(thisValue.aliasedUsers());
-    // If that is the case, method calls we want to fix up include users of those phis.
-    for (Phi phi : trivialPhis) {
-      users.addAll(phi.aliasedUsers());
-    }
-
-    fixupStaticizedValueUsers(code, users);
-
-    // We can't directly use Phi#removeTrivialPhi because they still refer to different operands.
-    trivialPhis.forEach(Phi::removeDeadPhi);
-
-    // No matter what, number of phi users should be zero too.
-    assert !thisValue.hasUsers() && !thisValue.hasPhiUsers();
-  }
-
-  // Re-processing finalized code may create slightly different IR code than what the examining
-  // phase has seen. For example,
-  //
-  //  b1:
-  //    s1 <- static-get singleton
-  //    ...
-  //    invoke-virtual { s1, ... } mtd1
-  //    goto Exit
-  //  b2:
-  //    s2 <- invoke-static getter()
-  //    ...
-  //    invoke-virtual { s2, ... } mtd1
-  //    goto Exit
-  //  ...
-  //  Exit: ...
-  //
-  // ~>
-  //
-  //  b1:
-  //    s1 <- static-get singleton
-  //    ...
-  //    goto Exit
-  //  b2:
-  //    s2 <- invoke-static getter()
-  //    ...
-  //    goto Exit
-  //  Exit:
-  //    sp <- phi(s1, s2)
-  //    invoke-virtual { sp, ... } mtd1
-  //    ...
-  //
-  // From staticizer's viewpoint, `sp` is trivial in the sense that it is composed of values that
-  // refer to the same singleton field. If so, we can safely relax the assertion; remove uses of
-  // field reads; remove quasi-trivial phis; and then remove original field reads.
-  private boolean testAndCollectPhisComposedOfSameMember(
-      Set<Phi> visited, Set<Phi> phisToCheck, DexMember dexMember, Set<Phi> trivialPhis) {
-    for (Phi phi : phisToCheck) {
-      if (!visited.add(phi)) {
-        continue;
-      }
-      Set<Phi> chainedPhis = Sets.newIdentityHashSet();
-      for (Value operand : phi.getOperands()) {
-        Value v = operand.getAliasedValue();
-        if (v.isPhi()) {
-          chainedPhis.add(operand.asPhi());
-        } else {
-          Instruction definition = v.definition;
-          if (!definition.isStaticGet() && !definition.isInvokeStatic()) {
-            return false;
-          }
-          if (definition.isStaticGet() && definition.asStaticGet().getField() != dexMember) {
-            return false;
-          } else if (definition.isInvokeStatic()
-              && definition.asInvokeStatic().getInvokedMethod() != dexMember) {
-            return false;
-          }
-        }
-      }
-      chainedPhis.addAll(phi.uniquePhiUsers());
-      if (!chainedPhis.isEmpty()) {
-        if (!testAndCollectPhisComposedOfSameMember(visited, chainedPhis, dexMember, trivialPhis)) {
-          return false;
-        }
-      }
-      trivialPhis.add(phi);
-    }
-    return true;
-  }
-
-  // Fixup field read usages. Same as {@link #fixupStaticizedThisUsers} except this one determines
-  // quasi-trivial phis, based on the original field.
-  private void fixupStaticizedFieldUsers(IRCode code, Value dest, DexMember member) {
-    assert dest != null;
-    // During the examine phase, field reads with any phi users have been invalidated, hence zero.
-    // However, it may be not true if re-processing introduces phis after optimizing common suffix.
-    Set<Phi> trivialPhis = Sets.newIdentityHashSet();
-    boolean onlyHasTrivialPhis =
-        testAndCollectPhisComposedOfSameMember(
-            Sets.newIdentityHashSet(), dest.uniquePhiUsers(), member, trivialPhis);
-    assert !dest.hasPhiUsers() || onlyHasTrivialPhis;
-    assert trivialPhis.isEmpty() || onlyHasTrivialPhis;
-
-    Set<Instruction> users = SetUtils.newIdentityHashSet(dest.aliasedUsers());
-    // If that is the case, method calls we want to fix up include users of those phis.
-    for (Phi phi : trivialPhis) {
-      users.addAll(phi.aliasedUsers());
-    }
-
-    fixupStaticizedValueUsers(code, users);
-
-    // We can't directly use Phi#removeTrivialPhi because they still refer to different operands.
-    trivialPhis.forEach(Phi::removeDeadPhi);
-
-    // No matter what, number of phi users should be zero too.
-    assert !dest.hasUsers() && !dest.hasPhiUsers();
-  }
-
-  private void fixupStaticizedValueUsers(IRCode code, Set<Instruction> users) {
-    for (Instruction user : users) {
-      if (user.isAssume()) {
-        continue;
-      }
-      assert user.isInvokeVirtual() || user.isInvokeDirect();
-      InvokeMethodWithReceiver invoke = user.asInvokeMethodWithReceiver();
-      Value newValue = null;
-      Value outValue = invoke.outValue();
-      if (outValue != null) {
-        newValue = code.createValue(outValue.getType());
-        DebugLocalInfo localInfo = outValue.getLocalInfo();
-        if (localInfo != null) {
-          newValue.setLocalInfo(localInfo);
-        }
-      }
-      List<Value> args = invoke.inValues();
-      invoke.replace(
-          new InvokeStatic(invoke.getInvokedMethod(), newValue, args.subList(1, args.size())),
-          code);
-    }
-  }
-
-  private void remapMovedCandidates(IRCode code) {
-    InstructionListIterator it = code.instructionListIterator();
-    while (it.hasNext()) {
-      Instruction instruction = it.next();
-
-      if (instruction.isStaticGet()) {
-        StaticGet staticGet = instruction.asStaticGet();
-        DexField field = mapFieldIfMoved(staticGet.getField());
-        if (field != staticGet.getField()) {
-          Value outValue = staticGet.dest();
-          assert outValue != null;
-          it.replaceCurrentInstruction(
-              new StaticGet(
-                  code.createValue(
-                      TypeElement.fromDexType(
-                          field.type, outValue.getType().nullability(), appView),
-                      outValue.getLocalInfo()),
-                  field));
-        }
-        continue;
-      }
-
-      if (instruction.isStaticPut()) {
-        StaticPut staticPut = instruction.asStaticPut();
-        DexField field = mapFieldIfMoved(staticPut.getField());
-        if (field != staticPut.getField()) {
-          it.replaceCurrentInstruction(new StaticPut(staticPut.value(), field));
-        }
-        continue;
-      }
-
-      if (instruction.isInvokeStatic()) {
-        InvokeStatic invoke = instruction.asInvokeStatic();
-        DexMethod method = invoke.getInvokedMethod();
-        DexType hostType = candidateToHostMapping.get(method.holder);
-        if (hostType != null) {
-          DexMethod newMethod = factory().createMethod(hostType, method.proto, method.name);
-          Value outValue = invoke.outValue();
-          DexType returnType = method.proto.returnType;
-          Value newOutValue =
-              returnType.isVoidType() || outValue == null
-                  ? null
-                  : code.createValue(
-                      TypeElement.fromDexType(
-                          returnType, outValue.getType().nullability(), appView),
-                      outValue.getLocalInfo());
-          it.replaceCurrentInstruction(new InvokeStatic(newMethod, newOutValue, invoke.inValues()));
-        }
-        continue;
-      }
-    }
-  }
-
-  private DexField mapFieldIfMoved(DexField field) {
-    DexType hostType = candidateToHostMapping.get(field.holder);
-    if (hostType != null) {
-      field = factory().createField(hostType, field.type, field.name);
-    }
-    hostType = candidateToHostMapping.get(field.type);
-    if (hostType != null) {
-      field = factory().createField(field.holder, hostType, field.name);
-    }
-    return field;
-  }
-
-  private ProgramMethodSet staticizeMethodSymbols() {
-    MutableBidirectionalOneToOneMap<DexMethod, DexMethod> methodMapping =
-        new BidirectionalOneToOneHashMap<>();
-    MutableBidirectionalOneToOneMap<DexField, DexField> fieldMapping =
-        new BidirectionalOneToOneHashMap<>();
-
-    ProgramMethodSet staticizedMethods = ProgramMethodSet.create();
-    for (CandidateInfo candidate : classStaticizer.candidates.values()) {
-      DexProgramClass candidateClass = candidate.candidate;
-
-      // Move instance methods into static ones.
-      List<DexEncodedMethod> newDirectMethods = new ArrayList<>();
-      for (DexEncodedMethod method : candidateClass.methods()) {
-        if (method.isStatic()) {
-          newDirectMethods.add(method);
-        } else if (!factory().isConstructor(method.getReference())) {
-          DexEncodedMethod staticizedMethod = method.toStaticMethodWithoutThis(appView);
-          newDirectMethods.add(staticizedMethod);
-          staticizedMethods.createAndAdd(candidateClass, staticizedMethod);
-          methodMapping.put(method.getReference(), staticizedMethod.getReference());
-        }
-      }
-      candidateClass.setVirtualMethods(DexEncodedMethod.EMPTY_ARRAY);
-      candidateClass.setDirectMethods(newDirectMethods.toArray(DexEncodedMethod.EMPTY_ARRAY));
-
-      // Consider moving static members from candidate into host.
-      DexType hostType = candidate.hostType();
-      if (candidateClass.type != hostType) {
-        DexProgramClass hostClass = asProgramClassOrNull(appView.definitionFor(hostType));
-        assert hostClass != null;
-        if (!classMembersConflict(candidateClass, hostClass)
-            && !hasMembersNotStaticized(candidateClass, staticizedMethods)) {
-          // Move all members of the candidate class into host class.
-          moveMembersIntoHost(staticizedMethods,
-              candidateClass, hostType, hostClass, methodMapping, fieldMapping);
-        }
-      }
-    }
-
-    if (!methodMapping.isEmpty() || !fieldMapping.isEmpty()) {
-      appView.setGraphLens(new ClassStaticizerGraphLens(appView, fieldMapping, methodMapping));
-    }
-    return staticizedMethods;
-  }
-
-  private boolean classMembersConflict(DexClass a, DexClass b) {
-    assert Streams.stream(a.methods()).allMatch(DexEncodedMethod::isStatic);
-    assert a.instanceFields().size() == 0;
-    return a.staticFields().stream().anyMatch(fld -> b.lookupField(fld.getReference()) != null)
-        || Streams.stream(a.methods())
-            .anyMatch(method -> b.lookupMethod(method.getReference()) != null);
-  }
-
-  private boolean hasMembersNotStaticized(
-      DexProgramClass candidateClass, ProgramMethodSet staticizedMethods) {
-    // TODO(b/159174309): Refine the analysis to allow for fields.
-    if (candidateClass.hasFields()) {
-      return true;
-    }
-    // TODO(b/158018192): Activate again when picking up all references.
-    return candidateClass.methods(not(staticizedMethods::contains)).iterator().hasNext();
-  }
-
-  private void moveMembersIntoHost(
-      ProgramMethodSet staticizedMethods,
-      DexProgramClass candidateClass,
-      DexType hostType,
-      DexProgramClass hostClass,
-      MutableBidirectionalOneToOneMap<DexMethod, DexMethod> methodMapping,
-      MutableBidirectionalOneToOneMap<DexField, DexField> fieldMapping) {
-    candidateToHostMapping.put(candidateClass.type, hostType);
-
-    // Process static fields.
-    int numOfHostStaticFields = hostClass.staticFields().size();
-    DexEncodedField[] newFields =
-        candidateClass.staticFields().size() > 0
-            ? new DexEncodedField[numOfHostStaticFields + candidateClass.staticFields().size()]
-            : new DexEncodedField[numOfHostStaticFields];
-    List<DexEncodedField> oldFields = hostClass.staticFields();
-    for (int i = 0; i < oldFields.size(); i++) {
-      DexEncodedField field = oldFields.get(i);
-      DexField newField = mapCandidateField(field.getReference(), candidateClass.type, hostType);
-      if (newField != field.getReference()) {
-        newFields[i] = field.toTypeSubstitutedField(appView, newField);
-        fieldMapping.put(field.getReference(), newField);
-      } else {
-        newFields[i] = field;
-      }
-    }
-    if (candidateClass.staticFields().size() > 0) {
-      List<DexEncodedField> extraFields = candidateClass.staticFields();
-      for (int i = 0; i < extraFields.size(); i++) {
-        DexEncodedField field = extraFields.get(i);
-        DexField newField = mapCandidateField(field.getReference(), candidateClass.type, hostType);
-        if (newField != field.getReference()) {
-          newFields[numOfHostStaticFields + i] = field.toTypeSubstitutedField(appView, newField);
-          fieldMapping.put(field.getReference(), newField);
-        } else {
-          newFields[numOfHostStaticFields + i] = field;
-        }
-      }
-    }
-    hostClass.setStaticFields(newFields);
-
-    // Process static methods.
-    if (!candidateClass.getMethodCollection().hasDirectMethods()) {
-      return;
-    }
-
-    Iterable<DexEncodedMethod> extraMethods = candidateClass.directMethods();
-    List<DexEncodedMethod> newMethods =
-        new ArrayList<>(candidateClass.getMethodCollection().numberOfDirectMethods());
-    for (DexEncodedMethod method : extraMethods) {
-      DexEncodedMethod newMethod =
-          method.toTypeSubstitutedMethod(
-              factory()
-                  .createMethod(hostType, method.getReference().proto, method.getReference().name));
-      newMethods.add(newMethod);
-      // If the old method from the candidate class has been staticized,
-      if (staticizedMethods.remove(method)) {
-        // Properly update staticized methods to reprocess, i.e., add the corresponding one that
-        // has just been migrated to the host class.
-        staticizedMethods.createAndAdd(hostClass, newMethod);
-      }
-      DexMethod originalMethod = methodMapping.getRepresentativeKey(method.getReference());
-      if (originalMethod == null) {
-        methodMapping.put(method.getReference(), newMethod.getReference());
-      } else {
-        methodMapping.put(originalMethod, newMethod.getReference());
-      }
-    }
-    hostClass.addDirectMethods(newMethods);
-  }
-
-  private DexField mapCandidateField(DexField field, DexType candidateType, DexType hostType) {
-    return field.holder != candidateType && field.type != candidateType ? field
-        : factory().createField(
-            field.holder == candidateType ? hostType : field.holder,
-            field.type == candidateType ? hostType : field.type,
-            field.name);
-  }
-
-  private DexItemFactory factory() {
-    return appView.dexItemFactory();
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
index 25667ea..89ac4ce 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
@@ -75,14 +75,17 @@
 
     if (invokeType.isSuper() && options.canHaveSuperInvokeBug()) {
       // To preserve semantics we should find the first library method on the boundary.
-      DexClass libraryHolder =
-          appView.definitionFor(
-              firstLibraryClassOrFirstInterfaceTarget(
-                  resolutionResult.getResolvedHolder(),
-                  appView,
-                  resolvedMethod.getReference(),
-                  original.getHolderType(),
-                  DexClass::lookupMethod));
+      DexType firstLibraryTarget =
+          firstLibraryClassOrFirstInterfaceTarget(
+              resolutionResult.getResolvedHolder(),
+              appView,
+              resolvedMethod.getReference(),
+              original.getHolderType(),
+              DexClass::lookupMethod);
+      if (firstLibraryTarget == null) {
+        return original;
+      }
+      DexClass libraryHolder = appView.definitionFor(firstLibraryTarget);
       if (libraryHolder == null) {
         return original;
       }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java
index 4b8bff6..96b68f3 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.VirtualRootMethodsAnalysis;
 import com.android.tools.r8.optimize.argumentpropagation.reprocessingcriteria.ArgumentPropagatorReprocessingCriteriaCollection;
+import com.android.tools.r8.optimize.argumentpropagation.unusedarguments.EffectivelyUnusedArgumentsAnalysis;
 import com.android.tools.r8.optimize.argumentpropagation.utils.ProgramClassesBidirectedGraph;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.ThreadUtils;
@@ -44,6 +45,9 @@
    */
   private ArgumentPropagatorCodeScanner codeScanner;
 
+  /** Collects and solves constraints for effectively unused argument removal. */
+  private EffectivelyUnusedArgumentsAnalysis effectivelyUnusedArgumentsAnalysis;
+
   /**
    * Analyzes the uses of arguments in methods to determine when reprocessing of methods will likely
    * not lead to any additional code optimizations.
@@ -70,6 +74,7 @@
 
     reprocessingCriteriaCollection = new ArgumentPropagatorReprocessingCriteriaCollection(appView);
     codeScanner = new ArgumentPropagatorCodeScanner(appView, reprocessingCriteriaCollection);
+    effectivelyUnusedArgumentsAnalysis = new EffectivelyUnusedArgumentsAnalysis(appView);
 
     ImmediateProgramSubtypingInfo immediateSubtypingInfo =
         ImmediateProgramSubtypingInfo.create(appView);
@@ -83,12 +88,16 @@
           // method state to unknown.
           new ArgumentPropagatorUnoptimizableMethods(
                   appView, immediateSubtypingInfo, codeScanner.getMethodStates())
-              .disableArgumentPropagationForUnoptimizableMethods(classes);
+              .initializeUnoptimizableMethodStates(classes);
 
           // Compute the mapping from virtual methods to their root virtual method and the set of
           // monomorphic virtual methods.
           new VirtualRootMethodsAnalysis(appView, immediateSubtypingInfo)
-              .extendVirtualRootMethods(classes, codeScanner);
+              .initializeVirtualRootMethods(classes, codeScanner);
+
+          // Find the virtual methods in the strongly connected component that only have monomorphic
+          // calls.
+          effectivelyUnusedArgumentsAnalysis.initializeOptimizableVirtualMethods(classes);
         },
         executorService);
 
@@ -103,10 +112,14 @@
       assert methodProcessor.isPrimaryMethodProcessor();
       codeScanner.scan(method, code, timing);
 
+      assert effectivelyUnusedArgumentsAnalysis != null;
+      effectivelyUnusedArgumentsAnalysis.scan(method, code);
+
       assert reprocessingCriteriaCollection != null;
       reprocessingCriteriaCollection.analyzeArgumentUses(method, code);
     } else {
       assert !methodProcessor.isPrimaryMethodProcessor();
+      assert effectivelyUnusedArgumentsAnalysis == null;
       assert !methodProcessor.isPostMethodProcessor() || reprocessingCriteriaCollection == null;
     }
   }
@@ -210,6 +223,11 @@
             interfaceDispatchOutsideProgram)
         .populateOptimizationInfo(converter, executorService, timing);
     timing.end();
+
+    timing.begin("Compute unused arguments");
+    effectivelyUnusedArgumentsAnalysis.computeEffectivelyUnusedArguments(codeScannerResult);
+    effectivelyUnusedArgumentsAnalysis = null;
+    timing.end();
   }
 
   /**
@@ -226,6 +244,9 @@
     assert codeScanner != null;
     MethodState methodState = codeScanner.getMethodStates().removeOrElse(method, null);
     assert methodState == null || method.getDefinition().belongsToDirectPool();
+
+    assert effectivelyUnusedArgumentsAnalysis != null;
+    effectivelyUnusedArgumentsAnalysis.onMethodPruned(method);
   }
 
   public void onMethodCodePruned(ProgramMethod method) {
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorGraphLens.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorGraphLens.java
index 866fdd3..ae5322e 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorGraphLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorGraphLens.java
@@ -35,6 +35,11 @@
     return new Builder(appView);
   }
 
+  @Override
+  public boolean isArgumentPropagatorGraphLens() {
+    return true;
+  }
+
   public boolean hasPrototypeChanges(DexMethod method) {
     return prototypeChanges.containsKey(method);
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorUnoptimizableMethods.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorUnoptimizableMethods.java
index 7ed397c..6319e90 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorUnoptimizableMethods.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorUnoptimizableMethods.java
@@ -36,7 +36,7 @@
 
   // TODO(b/190154391): Consider if we should bail out for classes that inherit from a missing
   //  class.
-  public void disableArgumentPropagationForUnoptimizableMethods(
+  public void initializeUnoptimizableMethodStates(
       Collection<DexProgramClass> stronglyConnectedComponent) {
     ProgramMethodSet unoptimizableVirtualMethods =
         MethodOverridesCollector.findAllMethodsAndOverridesThatMatches(
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodParameter.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodParameter.java
index 9f7434b..b9c09de 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodParameter.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodParameter.java
@@ -38,4 +38,9 @@
   public int hashCode() {
     return Objects.hash(method, index);
   }
+
+  @Override
+  public String toString() {
+    return "MethodParameter(" + method + ", " + index + ")";
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysis.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysis.java
index ed2596a..d33cfb8 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysis.java
@@ -114,7 +114,7 @@
     super(appView, immediateSubtypingInfo);
   }
 
-  public void extendVirtualRootMethods(
+  public void initializeVirtualRootMethods(
       Collection<DexProgramClass> stronglyConnectedComponent,
       ArgumentPropagatorCodeScanner codeScanner) {
     // Find all the virtual root methods in the strongly connected component.
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/lenscoderewriter/NullCheckInserter.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/lenscoderewriter/NullCheckInserter.java
new file mode 100644
index 0000000..3417bfa
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/lenscoderewriter/NullCheckInserter.java
@@ -0,0 +1,201 @@
+// Copyright (c) 2022, 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.argumentpropagation.lenscoderewriter;
+
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.FieldResolutionResult;
+import com.android.tools.r8.graph.GraphLens;
+import com.android.tools.r8.graph.GraphLens.MethodLookupResult;
+import com.android.tools.r8.graph.GraphLens.NonIdentityGraphLens;
+import com.android.tools.r8.graph.RewrittenPrototypeDescription.ArgumentInfo;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.BasicBlockIterator;
+import com.android.tools.r8.ir.code.FieldGet;
+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.InvokeMethod;
+import com.android.tools.r8.ir.code.InvokeStatic;
+import com.android.tools.r8.ir.code.Phi;
+import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.WorkList;
+import java.util.IdentityHashMap;
+import java.util.Map;
+
+public abstract class NullCheckInserter {
+
+  public static NullCheckInserter create(
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      IRCode code,
+      NonIdentityGraphLens graphLens,
+      GraphLens codeLens) {
+    NonIdentityGraphLens previousLens =
+        graphLens.find(lens -> lens.isArgumentPropagatorGraphLens() || lens == codeLens);
+    if (previousLens != null
+        && previousLens != codeLens
+        && previousLens.isArgumentPropagatorGraphLens()) {
+      return new NullCheckInserterImpl(appView.withLiveness(), code, graphLens);
+    }
+    return new EmptyNullCheckInserter();
+  }
+
+  public abstract void insertNullCheckForInvokeReceiverIfNeeded(
+      InvokeMethod invoke, InvokeMethod rewrittenInvoke, MethodLookupResult lookup);
+
+  public abstract void processWorklist();
+
+  static class NullCheckInserterImpl extends NullCheckInserter {
+
+    private final AppView<AppInfoWithLiveness> appView;
+    private final IRCode code;
+    private final NonIdentityGraphLens graphLens;
+
+    private final Map<InvokeStatic, Value> worklist = new IdentityHashMap<>();
+
+    NullCheckInserterImpl(
+        AppView<AppInfoWithLiveness> appView, IRCode code, NonIdentityGraphLens graphLens) {
+      this.appView = appView;
+      this.code = code;
+      this.graphLens = graphLens;
+    }
+
+    @Override
+    public void insertNullCheckForInvokeReceiverIfNeeded(
+        InvokeMethod invoke, InvokeMethod rewrittenInvoke, MethodLookupResult lookup) {
+      // If the invoke has been staticized, then synthesize a null check for the receiver.
+      if (!invoke.isInvokeMethodWithReceiver() || !rewrittenInvoke.isInvokeStatic()) {
+        return;
+      }
+
+      ArgumentInfo receiverArgumentInfo =
+          lookup.getPrototypeChanges().getArgumentInfoCollection().getArgumentInfo(0);
+      if (!receiverArgumentInfo.isRemovedArgumentInfo()
+          || !receiverArgumentInfo.asRemovedArgumentInfo().isCheckNullOrZeroSet()) {
+        return;
+      }
+
+      Value receiver = invoke.asInvokeMethodWithReceiver().getReceiver();
+      TypeElement receiverType = receiver.getType();
+      if (receiverType.isDefinitelyNotNull()) {
+        return;
+      }
+
+      // A parameter with users is only subject to effectively unused argument removal if it is
+      // guaranteed to be non-null.
+      if (receiver.isDefinedByInstructionSatisfying(Instruction::isUnusedArgument)) {
+        return;
+      }
+
+      worklist.put(rewrittenInvoke.asInvokeStatic(), receiver);
+    }
+
+    @Override
+    public void processWorklist() {
+      if (worklist.isEmpty()) {
+        return;
+      }
+
+      BasicBlockIterator blockIterator = code.listIterator();
+      while (blockIterator.hasNext()) {
+        BasicBlock block = blockIterator.next();
+        InstructionListIterator instructionIterator = block.listIterator(code);
+        while (instructionIterator.hasNext()) {
+          Instruction instruction = instructionIterator.next();
+          if (!instruction.isInvokeStatic()) {
+            continue;
+          }
+
+          InvokeStatic invoke = instruction.asInvokeStatic();
+          if (!worklist.containsKey(invoke)) {
+            continue;
+          }
+
+          // Don't insert null checks for effectively unread fields.
+          Value receiver = worklist.get(invoke);
+          if (isReadOfEffectivelyUnreadField(receiver)) {
+            continue;
+          }
+
+          instructionIterator.previous();
+
+          Position nullCheckPosition =
+              invoke
+                  .getPosition()
+                  .getOutermostCallerMatchingOrElse(
+                      Position::isRemoveInnerFramesIfThrowingNpe, invoke.getPosition());
+          instructionIterator.insertNullCheckInstruction(
+              appView, code, blockIterator, receiver, nullCheckPosition);
+
+          // Reset the block iterator.
+          if (invoke.getBlock().hasCatchHandlers()) {
+            BasicBlock splitBlock = invoke.getBlock();
+            BasicBlock previousBlock = blockIterator.previousUntil(b -> b == splitBlock);
+            assert previousBlock == splitBlock;
+            blockIterator.next();
+            instructionIterator = splitBlock.listIterator(code);
+          }
+
+          Instruction next = instructionIterator.next();
+          assert next == invoke;
+        }
+      }
+    }
+
+    private boolean isReadOfEffectivelyUnreadField(Value value) {
+      if (value.isPhi()) {
+        boolean hasSeenReadOfEffectivelyUnreadField = false;
+        WorkList<Phi> reachablePhis = WorkList.newIdentityWorkList(value.asPhi());
+        while (reachablePhis.hasNext()) {
+          Phi currentPhi = reachablePhis.next();
+          for (Value operand : currentPhi.getOperands()) {
+            if (operand.isPhi()) {
+              reachablePhis.addIfNotSeen(operand.asPhi());
+            } else if (!isReadOfEffectivelyUnreadField(operand.getDefinition())) {
+              return false;
+            } else {
+              hasSeenReadOfEffectivelyUnreadField = true;
+            }
+          }
+        }
+        assert hasSeenReadOfEffectivelyUnreadField;
+        return true;
+      } else {
+        return isReadOfEffectivelyUnreadField(value.getDefinition());
+      }
+    }
+
+    private boolean isReadOfEffectivelyUnreadField(Instruction instruction) {
+      if (instruction.isFieldGet()) {
+        FieldGet fieldGet = instruction.asFieldGet();
+        DexField field = fieldGet.getField();
+        // This needs to map the field all the way to the final graph lens.
+        DexField rewrittenField = appView.graphLens().lookupField(field, graphLens);
+        FieldResolutionResult resolutionResult = appView.appInfo().resolveField(rewrittenField);
+        return resolutionResult.isSuccessfulResolution()
+            && !appView.appInfo().isFieldRead(resolutionResult.getResolvedField());
+      }
+      return false;
+    }
+  }
+
+  static class EmptyNullCheckInserter extends NullCheckInserter {
+
+    @Override
+    public void insertNullCheckForInvokeReceiverIfNeeded(
+        InvokeMethod invoke, InvokeMethod rewrittenInvoke, MethodLookupResult lookup) {
+      // Intentionally empty.
+    }
+
+    @Override
+    public void processWorklist() {
+      // Intentionally empty.
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/unusedarguments/EffectivelyUnusedArgumentsAnalysis.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/unusedarguments/EffectivelyUnusedArgumentsAnalysis.java
new file mode 100644
index 0000000..c1a322b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/unusedarguments/EffectivelyUnusedArgumentsAnalysis.java
@@ -0,0 +1,227 @@
+// Copyright (c) 2022, 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.argumentpropagation.unusedarguments;
+
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethodSignature;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.Argument;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodParameter;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
+import com.android.tools.r8.optimize.argumentpropagation.utils.ParameterRemovalUtils;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.WorkList;
+import com.android.tools.r8.utils.collections.ProgramMethodSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Analysis to find arguments that are effectively unused. The analysis first computes the
+ * constraints for a given argument to be effectively unused, and then subsequently solves the
+ * computed constraints.
+ *
+ * <p>Example: Consider the following Companion class.
+ *
+ * <pre>
+ *   static class Companion {
+ *     void foo() {
+ *       this.bar()
+ *     }
+ *     void bar() {
+ *       doStuff();
+ *     }
+ *   }
+ * </pre>
+ *
+ * <p>The analysis works as follows.
+ *
+ * <ol>
+ *   <li>When IR processing the Companion.bar() method, the unused argument analysis records that
+ *       its receiver is unused.
+ *   <li>When IR processing the Companion.foo() method, the effectively unused argument analysis
+ *       records that the receiver of Companion.foo() is unused if the receiver of Companion.bar()
+ *       is unused.
+ *   <li>After IR processing all methods, the effectively unused argument analysis builds a graph
+ *       where there is a directed edge p0 -> p1 if the removal of method parameter p0 depends on
+ *       the removal of method parameter p1.
+ *   <li>The analysis then repeatedly removes method parameters from the graph that have no outgoing
+ *       edges and marks such method parameters as being effectively unused.
+ * </ol>
+ *
+ * <p>
+ */
+public class EffectivelyUnusedArgumentsAnalysis {
+
+  private final AppView<AppInfoWithLiveness> appView;
+
+  // Maps each method parameter p to the method parameters that must be effectively unused in order
+  // for the method parameter p to be effectively unused.
+  private final Map<MethodParameter, Set<MethodParameter>> constraints = new ConcurrentHashMap<>();
+
+  // Set of virtual methods that can definitely be optimized.
+  //
+  // We conservatively exclude virtual methods with dynamic dispatch from this set, since the
+  // parameters of such methods can only be removed if the same parameter can be removed from all
+  // other virtual methods with the same signature in the class hierarchy.
+  private final ProgramMethodSet optimizableVirtualMethods = ProgramMethodSet.createConcurrent();
+
+  public EffectivelyUnusedArgumentsAnalysis(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+  }
+
+  public void initializeOptimizableVirtualMethods(Set<DexProgramClass> stronglyConnectedComponent) {
+    // Group all virtual methods in this strongly connected component by their signature.
+    Map<DexMethodSignature, ProgramMethodSet> methodsBySignature = new HashMap<>();
+    for (DexProgramClass clazz : stronglyConnectedComponent) {
+      clazz.forEachProgramVirtualMethod(
+          method -> {
+            ProgramMethodSet methodsWithSameSignature =
+                methodsBySignature.computeIfAbsent(
+                    method.getMethodSignature(), ignoreKey(ProgramMethodSet::create));
+            methodsWithSameSignature.add(method);
+          });
+    }
+    // Mark the unique method signatures as being optimizable.
+    methodsBySignature.forEach(
+        (signature, methodsWithSignature) -> {
+          if (methodsWithSignature.size() == 1) {
+            ProgramMethod method = methodsWithSignature.getFirst();
+            if (ParameterRemovalUtils.canRemoveUnusedParametersFrom(appView, method)) {
+              optimizableVirtualMethods.add(method);
+            }
+          }
+        });
+  }
+
+  public void scan(ProgramMethod method, IRCode code) {
+    // If this method is not subject to optimization, then don't compute effectively unused
+    // constraints for the method parameters.
+    if (isUnoptimizable(method)) {
+      return;
+    }
+    Iterator<Argument> argumentIterator = code.argumentIterator();
+    while (argumentIterator.hasNext()) {
+      Argument argument = argumentIterator.next();
+      Value argumentValue = argument.outValue();
+      Set<MethodParameter> effectivelyUnusedConstraints =
+          computeEffectivelyUnusedConstraints(method, argument, argumentValue);
+      if (effectivelyUnusedConstraints != null && !effectivelyUnusedConstraints.isEmpty()) {
+        MethodParameter methodParameter =
+            new MethodParameter(method.getReference(), argument.getIndex());
+        assert !constraints.containsKey(methodParameter);
+        constraints.put(methodParameter, effectivelyUnusedConstraints);
+      }
+    }
+  }
+
+  private Set<MethodParameter> computeEffectivelyUnusedConstraints(
+      ProgramMethod method, Argument argument, Value argumentValue) {
+    if (method.getDefinition().isInstanceInitializer() && argumentValue.isThis()) {
+      return null;
+    }
+    if (method.getDefinition().willBeInlinedIntoInstanceInitializer(appView.dexItemFactory())) {
+      return null;
+    }
+    if (!ParameterRemovalUtils.canRemoveUnusedParameter(appView, method, argument.getIndex())) {
+      return null;
+    }
+    if (!argumentValue.getType().isClassType()
+        || argumentValue.hasDebugUsers()
+        || argumentValue.hasPhiUsers()) {
+      return null;
+    }
+    Set<MethodParameter> effectivelyUnusedConstraints = new HashSet<>();
+    for (Instruction user : argumentValue.uniqueUsers()) {
+      if (user.isInvokeMethod()) {
+        InvokeMethod invoke = user.asInvokeMethod();
+        ProgramMethod resolvedMethod =
+            appView
+                .appInfo()
+                .unsafeResolveMethodDueToDexFormat(invoke.getInvokedMethod())
+                .getResolvedProgramMethod();
+        if (resolvedMethod == null || isUnoptimizable(resolvedMethod)) {
+          return null;
+        }
+        int dependentArgumentIndex =
+            ListUtils.uniqueIndexMatching(invoke.arguments(), value -> value == argumentValue);
+        if (dependentArgumentIndex < 0
+            || !ParameterRemovalUtils.canRemoveUnusedParameter(
+                appView, resolvedMethod, dependentArgumentIndex)) {
+          return null;
+        }
+        effectivelyUnusedConstraints.add(
+            new MethodParameter(resolvedMethod.getReference(), dependentArgumentIndex));
+      } else {
+        return null;
+      }
+    }
+    return effectivelyUnusedConstraints;
+  }
+
+  public void computeEffectivelyUnusedArguments(MethodStateCollectionByReference methodStates) {
+    // Build a graph where nodes are method parameters and there is an edge from method parameter p0
+    // to method parameter p1 if the removal of p0 depends on the removal of p1.
+    EffectivelyUnusedArgumentsGraph dependenceGraph =
+        EffectivelyUnusedArgumentsGraph.create(appView, constraints, methodStates);
+
+    // Remove all unoptimizable method parameters from the graph, as well as all nodes that depend
+    // on a node that is unoptimable.
+    dependenceGraph.removeUnoptimizableNodes();
+
+    // Repeatedly mark method parameters with no outgoing edges (i.e., no dependencies) as being
+    // unused.
+    WorkList<EffectivelyUnusedArgumentsGraphNode> worklist =
+        WorkList.newIdentityWorkList(dependenceGraph.getNodes());
+    while (!worklist.isEmpty()) {
+      while (!worklist.isEmpty()) {
+        EffectivelyUnusedArgumentsGraphNode node = worklist.removeSeen();
+        assert dependenceGraph.verifyContains(node);
+        node.removeUnusedSuccessors();
+        if (node.getSuccessors().isEmpty()) {
+          node.setUnused();
+          node.getPredecessors().forEach(worklist::addIfNotSeen);
+          node.cleanForRemoval();
+          dependenceGraph.remove(node);
+        }
+      }
+
+      // Handle mutually recursive methods. If there is a cycle p0 -> p1 -> ... -> pn -> p0 and each
+      // of the method parameters p0 ... pn has a unique successor, then remove the edge pn -> p0
+      // from the graph.
+      dependenceGraph.removeClosedCycles(worklist::addIfNotSeen);
+    }
+  }
+
+  private boolean isUnoptimizable(ProgramMethod method) {
+    if (method.getDefinition().belongsToDirectPool()) {
+      return !ParameterRemovalUtils.canRemoveUnusedParametersFrom(appView, method);
+    }
+    if (optimizableVirtualMethods.contains(method)) {
+      assert ParameterRemovalUtils.canRemoveUnusedParametersFrom(appView, method);
+      return false;
+    }
+    return true;
+  }
+
+  public void onMethodPruned(ProgramMethod method) {
+    for (int argumentIndex = 0;
+        argumentIndex < method.getDefinition().getNumberOfArguments();
+        argumentIndex++) {
+      constraints.remove(new MethodParameter(method.getReference(), argumentIndex));
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/unusedarguments/EffectivelyUnusedArgumentsGraph.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/unusedarguments/EffectivelyUnusedArgumentsGraph.java
new file mode 100644
index 0000000..fdc01bc
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/unusedarguments/EffectivelyUnusedArgumentsGraph.java
@@ -0,0 +1,183 @@
+// Copyright (c) 2022, 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.argumentpropagation.unusedarguments;
+
+import static com.android.tools.r8.graph.DexClassAndMethod.asProgramMethodOrNull;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodParameter;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.WorkList;
+import com.android.tools.r8.utils.dfs.DFSStack;
+import com.android.tools.r8.utils.dfs.DFSWorklistItem;
+import com.android.tools.r8.utils.dfs.DFSWorklistItem.NewlyVisitedDFSWorklistItem;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import java.util.ArrayDeque;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+
+class EffectivelyUnusedArgumentsGraph {
+
+  private final AppView<AppInfoWithLiveness> appView;
+
+  private final Map<MethodParameter, EffectivelyUnusedArgumentsGraphNode> nodes = new HashMap<>();
+
+  private EffectivelyUnusedArgumentsGraph(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+  }
+
+  public static EffectivelyUnusedArgumentsGraph create(
+      AppView<AppInfoWithLiveness> appView,
+      Map<MethodParameter, Set<MethodParameter>> constraints,
+      MethodStateCollectionByReference methodStates) {
+    EffectivelyUnusedArgumentsGraph graph = new EffectivelyUnusedArgumentsGraph(appView);
+    constraints.forEach(
+        (methodParameter, constraintsForMethodParameter) -> {
+          EffectivelyUnusedArgumentsGraphNode node = graph.getOrCreateNode(methodParameter);
+          for (MethodParameter constraint : constraintsForMethodParameter) {
+            graph.addConstraintEdge(node, constraint, constraints, methodStates);
+          }
+        });
+    return graph;
+  }
+
+  void addConstraintEdge(
+      EffectivelyUnusedArgumentsGraphNode node,
+      MethodParameter constraint,
+      Map<MethodParameter, Set<MethodParameter>> constraints,
+      MethodStateCollectionByReference methodStates) {
+    ProgramMethod dependencyMethod =
+        asProgramMethodOrNull(appView.definitionFor(constraint.getMethod()));
+    if (dependencyMethod == null) {
+      assert false;
+      node.setUnoptimizable();
+      return;
+    }
+
+    // A nullable method parameter cannot be effectively unused if it is used as the receiver in an
+    // invoke (or we cannot preserve NPE semantics).
+    if (dependencyMethod.getDefinition().isInstance()
+        && constraint.getIndex() == 0
+        && node.isNullable(methodStates)) {
+      node.setUnoptimizable();
+      return;
+    }
+
+    // If the successor parameter does not have any constraints, then the successor is not subject
+    // to effectively unused argument removal. In this case, the successor can only be removed if it
+    // is truly unused.
+    if (!constraints.containsKey(constraint)) {
+      MethodOptimizationInfo optimizationInfo = dependencyMethod.getOptimizationInfo();
+      if (!optimizationInfo.hasUnusedArguments()
+          || !optimizationInfo.getUnusedArguments().get(constraint.getIndex())) {
+        node.setUnoptimizable();
+      }
+      return;
+    }
+
+    EffectivelyUnusedArgumentsGraphNode successor = getOrCreateNode(constraint, dependencyMethod);
+    if (node != successor) {
+      node.addSuccessor(successor);
+    }
+  }
+
+  Collection<EffectivelyUnusedArgumentsGraphNode> getNodes() {
+    return nodes.values();
+  }
+
+  EffectivelyUnusedArgumentsGraphNode getOrCreateNode(MethodParameter parameter) {
+    ProgramMethod method = asProgramMethodOrNull(appView.definitionFor(parameter.getMethod()));
+    return method != null ? getOrCreateNode(parameter, method) : null;
+  }
+
+  EffectivelyUnusedArgumentsGraphNode getOrCreateNode(
+      MethodParameter parameter, ProgramMethod method) {
+    return nodes.computeIfAbsent(
+        parameter, p -> new EffectivelyUnusedArgumentsGraphNode(method, p.getIndex()));
+  }
+
+  void remove(EffectivelyUnusedArgumentsGraphNode node) {
+    assert node.getSuccessors().isEmpty();
+    assert node.getPredecessors().isEmpty();
+    MethodParameter methodParameter =
+        new MethodParameter(node.getMethod().getReference(), node.getArgumentIndex());
+    EffectivelyUnusedArgumentsGraphNode removed = nodes.remove(methodParameter);
+    assert removed == node;
+  }
+
+  void removeClosedCycles(Consumer<EffectivelyUnusedArgumentsGraphNode> reprocess) {
+    Set<EffectivelyUnusedArgumentsGraphNode> seen = Sets.newIdentityHashSet();
+    for (EffectivelyUnusedArgumentsGraphNode root : getNodes()) {
+      if (seen.contains(root)) {
+        continue;
+      }
+      DFSStack<EffectivelyUnusedArgumentsGraphNode> stack = DFSStack.createIdentityStack();
+      Deque<DFSWorklistItem<EffectivelyUnusedArgumentsGraphNode>> worklist = new ArrayDeque<>();
+      worklist.add(new NewlyVisitedDFSWorklistItem<>(root));
+      while (!worklist.isEmpty()) {
+        DFSWorklistItem<EffectivelyUnusedArgumentsGraphNode> item = worklist.removeLast();
+        stack.handle(item);
+        if (item.isFullyVisited()) {
+          continue;
+        }
+        EffectivelyUnusedArgumentsGraphNode node = item.getValue();
+        seen.add(node);
+        worklist.add(item.asNewlyVisited().toFullyVisited());
+        node.getSuccessors()
+            .removeIf(
+                successor -> {
+                  if (stack.contains(successor)) {
+                    Deque<EffectivelyUnusedArgumentsGraphNode> cycle =
+                        stack.getCycleStartingAt(successor);
+                    boolean isClosedCycle =
+                        Iterables.all(cycle, member -> member.getSuccessors().size() == 1);
+                    if (isClosedCycle) {
+                      // Remove edge and reprocess this node, since it is now eligible for unused
+                      // argument removal.
+                      boolean removed = successor.getPredecessors().remove(node);
+                      assert removed;
+                      reprocess.accept(node);
+                      // Return true to remove the successor from the successors of the current
+                      // node (to prevent ConcurrentModificationException).
+                      return true;
+                    }
+                  } else {
+                    worklist.add(new NewlyVisitedDFSWorklistItem<>(successor));
+                  }
+                  return false;
+                });
+      }
+    }
+  }
+
+  boolean verifyContains(EffectivelyUnusedArgumentsGraphNode node) {
+    MethodParameter methodParameter =
+        new MethodParameter(node.getMethod().getReference(), node.getArgumentIndex());
+    return nodes.containsKey(methodParameter);
+  }
+
+  void removeUnoptimizableNodes() {
+    WorkList<EffectivelyUnusedArgumentsGraphNode> worklist = WorkList.newIdentityWorkList();
+    for (EffectivelyUnusedArgumentsGraphNode node : getNodes()) {
+      if (node.isUnoptimizable()) {
+        worklist.addIfNotSeen(node);
+      }
+    }
+    while (worklist.hasNext()) {
+      EffectivelyUnusedArgumentsGraphNode node = worklist.next();
+      worklist.addIfNotSeen(node.getPredecessors());
+      node.cleanForRemoval();
+      remove(node);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/unusedarguments/EffectivelyUnusedArgumentsGraphNode.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/unusedarguments/EffectivelyUnusedArgumentsGraphNode.java
new file mode 100644
index 0000000..2b52686
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/unusedarguments/EffectivelyUnusedArgumentsGraphNode.java
@@ -0,0 +1,137 @@
+// Copyright (c) 2022, 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.argumentpropagation.unusedarguments;
+
+import static com.android.tools.r8.ir.optimize.info.OptimizationFeedback.getSimpleFeedback;
+
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMonomorphicMethodState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ParameterState;
+import com.google.common.collect.Sets;
+import java.util.BitSet;
+import java.util.Set;
+
+class EffectivelyUnusedArgumentsGraphNode {
+
+  private final ProgramMethod method;
+  private final int argumentIndex;
+
+  private final Set<EffectivelyUnusedArgumentsGraphNode> predecessors = Sets.newIdentityHashSet();
+  private final Set<EffectivelyUnusedArgumentsGraphNode> successors = Sets.newIdentityHashSet();
+
+  private boolean unoptimizable;
+
+  EffectivelyUnusedArgumentsGraphNode(ProgramMethod method, int argumentIndex) {
+    this.method = method;
+    this.argumentIndex = argumentIndex;
+  }
+
+  void addSuccessor(EffectivelyUnusedArgumentsGraphNode node) {
+    if (successors.add(node)) {
+      node.predecessors.add(this);
+    } else {
+      assert node.predecessors.contains(this);
+    }
+  }
+
+  void cleanForRemoval() {
+    clearSuccessors();
+    clearPredecessors();
+  }
+
+  void clearSuccessors() {
+    for (EffectivelyUnusedArgumentsGraphNode successor : successors) {
+      boolean removed = successor.predecessors.remove(this);
+      assert removed;
+    }
+    successors.clear();
+  }
+
+  void clearPredecessors() {
+    for (EffectivelyUnusedArgumentsGraphNode predecessor : predecessors) {
+      boolean removed = predecessor.successors.remove(this);
+      assert removed;
+    }
+    predecessors.clear();
+  }
+
+  public int getArgumentIndex() {
+    return argumentIndex;
+  }
+
+  public ProgramMethod getMethod() {
+    return method;
+  }
+
+  Set<EffectivelyUnusedArgumentsGraphNode> getPredecessors() {
+    return predecessors;
+  }
+
+  Set<EffectivelyUnusedArgumentsGraphNode> getSuccessors() {
+    return successors;
+  }
+
+  boolean isNullable(MethodStateCollectionByReference methodStates) {
+    if (method.getDefinition().isInstance() && argumentIndex == 0) {
+      return false;
+    }
+    MethodState methodState = methodStates.get(method);
+    if (methodState.isBottom()) {
+      // TODO: this means the method is unreachable? what to do in this case?
+      return true;
+    }
+    assert !methodState.isBottom();
+    if (methodState.isUnknown()) {
+      return true;
+    }
+    assert methodState.isMonomorphic();
+    ConcreteMonomorphicMethodState monomorphicMethodState = methodState.asMonomorphic();
+    ParameterState parameterState = monomorphicMethodState.getParameterState(argumentIndex);
+    if (parameterState.isUnknown()) {
+      return true;
+    }
+    assert parameterState.isConcrete();
+    assert parameterState.asConcrete().isReferenceParameter();
+    return parameterState.asConcrete().asReferenceParameter().getNullability().isMaybeNull();
+  }
+
+  boolean isUnoptimizable() {
+    return unoptimizable;
+  }
+
+  boolean isUnused() {
+    return method.getOptimizationInfo().hasUnusedArguments()
+        && method.getOptimizationInfo().getUnusedArguments().get(argumentIndex);
+  }
+
+  void removeUnusedSuccessors() {
+    successors.removeIf(
+        successor -> {
+          if (successor.isUnused()) {
+            boolean removed = successor.predecessors.remove(this);
+            assert removed;
+            return true;
+          }
+          return false;
+        });
+  }
+
+  void setUnoptimizable() {
+    unoptimizable = true;
+  }
+
+  void setUnused() {
+    if (method.getOptimizationInfo().hasUnusedArguments()) {
+      getSimpleFeedback()
+          .fixupUnusedArguments(method, unusedArguments -> unusedArguments.set(argumentIndex));
+    } else {
+      BitSet unusedArguments = new BitSet(method.getDefinition().getNumberOfArguments());
+      unusedArguments.set(argumentIndex);
+      getSimpleFeedback().setUnusedArguments(method, unusedArguments);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/ParameterRemovalUtils.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/ParameterRemovalUtils.java
index 72cbfba..72129db 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/ParameterRemovalUtils.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/ParameterRemovalUtils.java
@@ -22,7 +22,8 @@
     if (appView.appInfo().isKeepUnusedArgumentsMethod(method)) {
       return false;
     }
-    return !appView.appInfoWithLiveness().isMethodTargetedByInvokeDynamic(method);
+    return method.getDefinition().isLibraryMethodOverride().isFalse()
+        && !appView.appInfoWithLiveness().isMethodTargetedByInvokeDynamic(method);
   }
 
   public static boolean canRemoveUnusedParameter(
diff --git a/src/main/java/com/android/tools/r8/utils/DepthFirstSearchWorkListBase.java b/src/main/java/com/android/tools/r8/utils/DepthFirstSearchWorkListBase.java
new file mode 100644
index 0000000..c82f332
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/DepthFirstSearchWorkListBase.java
@@ -0,0 +1,242 @@
+// Copyright (c) 2022, 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.utils;
+
+import static com.android.tools.r8.utils.DepthFirstSearchWorkListBase.ProcessingState.FINISHED;
+import static com.android.tools.r8.utils.DepthFirstSearchWorkListBase.ProcessingState.NOT_PROCESSED;
+import static com.android.tools.r8.utils.DepthFirstSearchWorkListBase.ProcessingState.WAITING;
+
+import com.android.tools.r8.utils.DepthFirstSearchWorkListBase.DFSNodeImpl;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+public abstract class DepthFirstSearchWorkListBase<N, T extends DFSNodeImpl<N>> {
+
+  public interface DFSNode<N> {
+    N getNode();
+
+    boolean seenAndNotProcessed();
+  }
+
+  public interface DFSNodeWithState<N, S> extends DFSNode<N> {
+
+    S getState();
+
+    void setState(S backtrackState);
+
+    boolean hasState();
+  }
+
+  enum ProcessingState {
+    NOT_PROCESSED,
+    WAITING,
+    FINISHED;
+  }
+
+  static class DFSNodeImpl<N> implements DFSNode<N> {
+
+    private final N node;
+    private ProcessingState processingState = NOT_PROCESSED;
+
+    private DFSNodeImpl(N node) {
+      this.node = node;
+    }
+
+    boolean isNotProcessed() {
+      return processingState == NOT_PROCESSED;
+    }
+
+    boolean isFinished() {
+      return processingState == FINISHED;
+    }
+
+    void setWaiting() {
+      processingState = WAITING;
+    }
+
+    void setFinished() {
+      assert processingState != FINISHED;
+      processingState = FINISHED;
+    }
+
+    @Override
+    public N getNode() {
+      return node;
+    }
+
+    @Override
+    public boolean seenAndNotProcessed() {
+      return processingState == WAITING;
+    }
+  }
+
+  static class DFSNodeWithStateImpl<N, S> extends DFSNodeImpl<N> implements DFSNodeWithState<N, S> {
+
+    private S state;
+
+    private DFSNodeWithStateImpl(N node) {
+      super(node);
+    }
+
+    @Override
+    public S getState() {
+      return state;
+    }
+
+    @Override
+    public void setState(S state) {
+      this.state = state;
+    }
+
+    @Override
+    public boolean hasState() {
+      return state != null;
+    }
+  }
+
+  private final ArrayDeque<T> workList = new ArrayDeque<>();
+
+  abstract T createDfsNode(N node);
+
+  /** The initial processing of a node during forward search */
+  abstract TraversalContinuation internalOnVisit(T node);
+
+  /** The joining of state during backtracking of the algorithm. */
+  abstract TraversalContinuation internalOnJoin(T node);
+
+  final T internalEnqueueNode(N value) {
+    T dfsNode = createDfsNode(value);
+    if (dfsNode.isNotProcessed()) {
+      workList.addLast(dfsNode);
+    }
+    return dfsNode;
+  }
+
+  @SafeVarargs
+  public final TraversalContinuation run(N... roots) {
+    return run(Arrays.asList(roots));
+  }
+
+  public final TraversalContinuation run(Collection<N> roots) {
+    roots.forEach(this::internalEnqueueNode);
+    TraversalContinuation continuation = TraversalContinuation.CONTINUE;
+    while (!workList.isEmpty()) {
+      T node = workList.removeLast();
+      if (node.isFinished()) {
+        continue;
+      }
+      if (node.isNotProcessed()) {
+        workList.addLast(node);
+        node.setWaiting();
+        continuation = internalOnVisit(node);
+      } else {
+        assert node.seenAndNotProcessed();
+        continuation = internalOnJoin(node);
+        node.setFinished();
+      }
+      if (continuation.shouldBreak()) {
+        return continuation;
+      }
+    }
+    return continuation;
+  }
+
+  public abstract static class DepthFirstSearchWorkList<N>
+      extends DepthFirstSearchWorkListBase<N, DFSNodeImpl<N>> {
+
+    /**
+     * The initial processing of the node when visiting the first time during the depth first
+     * search.
+     *
+     * @param node The current node.
+     * @param childNodeConsumer A consumer for adding child nodes. If an element has been seen
+     *     before but not finished there is a cycle.
+     * @return A value describing if the DFS algorithm should continue to run.
+     */
+    protected abstract TraversalContinuation process(
+        DFSNode<N> node, Function<N, DFSNode<N>> childNodeConsumer);
+
+    @Override
+    DFSNodeImpl<N> createDfsNode(N node) {
+      return new DFSNodeImpl<>(node);
+    }
+
+    @Override
+    TraversalContinuation internalOnVisit(DFSNodeImpl<N> node) {
+      return process(node, this::internalEnqueueNode);
+    }
+
+    @Override
+    protected TraversalContinuation internalOnJoin(DFSNodeImpl<N> node) {
+      return TraversalContinuation.CONTINUE;
+    }
+  }
+
+  public abstract static class StatefulDepthFirstSearchWorkList<N, S>
+      extends DepthFirstSearchWorkListBase<N, DFSNodeWithStateImpl<N, S>> {
+
+    private final Map<DFSNodeWithStateImpl<N, S>, List<DFSNodeWithState<N, S>>> childStateMap =
+        new IdentityHashMap<>();
+
+    /**
+     * The initial processing of the node when visiting the first time during the depth first
+     * search.
+     *
+     * @param node The current node.
+     * @param childNodeConsumer A consumer for adding child nodes. If an element has been seen
+     *     before but not finished there is a cycle.
+     * @return A value describing if the DFS algorithm should continue to run.
+     */
+    protected abstract TraversalContinuation process(
+        DFSNodeWithState<N, S> node, Function<N, DFSNodeWithState<N, S>> childNodeConsumer);
+
+    /**
+     * The joining of state during backtracking of the algorithm.
+     *
+     * @param node The current node
+     * @param childStates The already computed child states.
+     * @return A value describing if the DFS algorithm should continue to run.
+     */
+    protected abstract TraversalContinuation joiner(
+        DFSNodeWithState<N, S> node, List<DFSNodeWithState<N, S>> childStates);
+
+    @Override
+    DFSNodeWithStateImpl<N, S> createDfsNode(N node) {
+      return new DFSNodeWithStateImpl<>(node);
+    }
+
+    @Override
+    TraversalContinuation internalOnVisit(DFSNodeWithStateImpl<N, S> node) {
+      List<DFSNodeWithState<N, S>> childStates = new ArrayList<>();
+      List<DFSNodeWithState<N, S>> removedChildStates = childStateMap.put(node, childStates);
+      assert removedChildStates == null;
+      return process(
+          node,
+          successor -> {
+            DFSNodeWithStateImpl<N, S> successorNode = internalEnqueueNode(successor);
+            childStates.add(successorNode);
+            return successorNode;
+          });
+    }
+
+    @Override
+    protected TraversalContinuation internalOnJoin(DFSNodeWithStateImpl<N, S> node) {
+      return joiner(
+          node,
+          childStateMap.computeIfAbsent(
+              node,
+              n -> {
+                assert false : "Unexpected joining of not visited node";
+                return new ArrayList<>();
+              }));
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index d1264f6..d32fe5c 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -241,7 +241,6 @@
   public void disableGlobalOptimizations() {
     inlinerOptions.enableInlining = false;
     enableClassInlining = false;
-    enableClassStaticizer = false;
     enableDevirtualization = false;
     enableVerticalClassMerging = false;
     enableEnumUnboxing = false;
diff --git a/src/main/java/com/android/tools/r8/utils/ListUtils.java b/src/main/java/com/android/tools/r8/utils/ListUtils.java
index cd62dc2..9c6e2a7 100644
--- a/src/main/java/com/android/tools/r8/utils/ListUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ListUtils.java
@@ -279,6 +279,21 @@
     assert verifyComparatorOnSortedList(items, comparator);
   }
 
+  public static <T> int uniqueIndexMatching(List<T> list, Predicate<T> predicate) {
+    int result = -1;
+    for (int i = 0; i < list.size(); i++) {
+      T element = list.get(i);
+      if (predicate.test(element)) {
+        if (result == -1) {
+          result = i;
+        } else {
+          return -1;
+        }
+      }
+    }
+    return result;
+  }
+
   private static <T> boolean verifyComparatorOnSortedList(List<T> items, Comparator<T> comparator) {
     for (int i = 0; i < items.size(); i++) {
       boolean allowEqual = true;
diff --git a/src/main/java/com/android/tools/r8/utils/WorkList.java b/src/main/java/com/android/tools/r8/utils/WorkList.java
index 3177689..79c2ed7 100644
--- a/src/main/java/com/android/tools/r8/utils/WorkList.java
+++ b/src/main/java/com/android/tools/r8/utils/WorkList.java
@@ -92,6 +92,18 @@
     return false;
   }
 
+  public boolean addFirstIfNotSeen(T item) {
+    if (seen.add(item)) {
+      workingList.addFirst(item);
+      return true;
+    }
+    return false;
+  }
+
+  public void addFirstIgnoringSeenSet(T item) {
+    workingList.addFirst(item);
+  }
+
   public boolean hasNext() {
     return !workingList.isEmpty();
   }
@@ -117,6 +129,12 @@
     return workingList.removeFirst();
   }
 
+  public T removeSeen() {
+    T next = next();
+    seen.remove(next);
+    return next;
+  }
+
   public Set<T> getSeenSet() {
     return Collections.unmodifiableSet(seen);
   }
diff --git a/src/main/java/com/android/tools/r8/utils/dfs/DFSStack.java b/src/main/java/com/android/tools/r8/utils/dfs/DFSStack.java
new file mode 100644
index 0000000..635d9a6
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/dfs/DFSStack.java
@@ -0,0 +1,69 @@
+// Copyright (c) 2022, 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.utils.dfs;
+
+import com.google.common.collect.Sets;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.Set;
+
+public class DFSStack<T> {
+
+  private final Deque<T> stack;
+  private final Set<T> stackSet;
+
+  private DFSStack(Deque<T> stack, Set<T> stackSet) {
+    this.stack = stack;
+    this.stackSet = stackSet;
+  }
+
+  public static <T> DFSStack<T> createIdentityStack() {
+    return new DFSStack<>(new ArrayDeque<>(), Sets.newIdentityHashSet());
+  }
+
+  public boolean contains(T item) {
+    return stackSet.contains(item);
+  }
+
+  public Deque<T> getCycleStartingAt(T entry) {
+    Deque<T> cycle = new ArrayDeque<>();
+    do {
+      assert !stack.isEmpty();
+      cycle.addLast(stack.removeLast());
+    } while (cycle.getLast() != entry);
+    recoverStack(cycle);
+    return cycle;
+  }
+
+  public void handle(DFSWorklistItem<T> item) {
+    if (item.isNewlyVisited()) {
+      push(item.getValue());
+    } else {
+      assert item.isFullyVisited();
+      pop(item.getValue());
+    }
+  }
+
+  public void pop(T expectedItem) {
+    T popped = stack.removeLast();
+    assert popped == expectedItem;
+    boolean removed = stackSet.remove(popped);
+    assert removed;
+  }
+
+  public void push(T item) {
+    stack.addLast(item);
+    boolean added = stackSet.add(item);
+    assert added;
+  }
+
+  private void recoverStack(Deque<T> extractedCycle) {
+    Iterator<T> descendingIt = extractedCycle.descendingIterator();
+    while (descendingIt.hasNext()) {
+      stack.addLast(descendingIt.next());
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/dfs/DFSWorklistItem.java b/src/main/java/com/android/tools/r8/utils/dfs/DFSWorklistItem.java
new file mode 100644
index 0000000..4020cea
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/dfs/DFSWorklistItem.java
@@ -0,0 +1,63 @@
+// Copyright (c) 2022, 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.utils.dfs;
+
+public abstract class DFSWorklistItem<T> {
+
+  T value;
+
+  public DFSWorklistItem(T value) {
+    this.value = value;
+  }
+
+  public T getValue() {
+    return value;
+  }
+
+  public boolean isFullyVisited() {
+    return false;
+  }
+
+  public boolean isNewlyVisited() {
+    return false;
+  }
+
+  public NewlyVisitedDFSWorklistItem<T> asNewlyVisited() {
+    return null;
+  }
+
+  public static class NewlyVisitedDFSWorklistItem<T> extends DFSWorklistItem<T> {
+
+    public NewlyVisitedDFSWorklistItem(T value) {
+      super(value);
+    }
+
+    @Override
+    public boolean isNewlyVisited() {
+      return true;
+    }
+
+    @Override
+    public NewlyVisitedDFSWorklistItem<T> asNewlyVisited() {
+      return this;
+    }
+
+    public FullyVisitedDFSWorklistItem<T> toFullyVisited() {
+      return new FullyVisitedDFSWorklistItem<>(getValue());
+    }
+  }
+
+  public static class FullyVisitedDFSWorklistItem<T> extends DFSWorklistItem<T> {
+
+    public FullyVisitedDFSWorklistItem(T value) {
+      super(value);
+    }
+
+    @Override
+    public boolean isFullyVisited() {
+      return true;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index b3a595a..ce82006 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -469,17 +469,6 @@
         options -> options.testing.allowClassInliningOfSynthetics = false);
   }
 
-  public T noClassStaticizing() {
-    return noClassStaticizing(true);
-  }
-
-  public T noClassStaticizing(boolean condition) {
-    if (condition) {
-      return addOptionsModification(options -> options.enableClassStaticizer = false);
-    }
-    return self();
-  }
-
   public T noHorizontalClassMerging() {
     return noHorizontalClassMerging(true);
   }
diff --git a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
index be94525..ac6ca8e 100644
--- a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -34,6 +34,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.function.Consumer;
@@ -79,7 +80,7 @@
   private PrintStream oldStderr = null;
   protected OutputMode outputMode = OutputMode.DexIndexed;
 
-  private boolean isAndroidBuildVersionAdded = false;
+  private Optional<Integer> isAndroidBuildVersionAdded = null;
 
   LibraryDesugaringTestConfiguration libraryDesugaringTestConfiguration =
       LibraryDesugaringTestConfiguration.DISABLED;
@@ -89,8 +90,13 @@
   }
 
   public T addAndroidBuildVersion() {
+    return addAndroidBuildVersion(null);
+  }
+
+  public T addAndroidBuildVersion(AndroidApiLevel specifiedApiLevel) {
     addProgramClasses(AndroidBuildVersion.class);
-    isAndroidBuildVersionAdded = true;
+    isAndroidBuildVersionAdded =
+        Optional.ofNullable(specifiedApiLevel == null ? null : specifiedApiLevel.getLevel());
     return self();
   }
 
@@ -257,8 +263,10 @@
       cr =
           internalCompile(builder, optionsConsumer, Suppliers.memoize(sink::build), benchmark)
               .addRunClasspathFiles(additionalRunClassPath);
-      if (isAndroidBuildVersionAdded) {
-        cr.setSystemProperty(AndroidBuildVersion.PROPERTY, "" + builder.getMinApiLevel());
+      if (isAndroidBuildVersionAdded != null) {
+        cr.setSystemProperty(
+            AndroidBuildVersion.PROPERTY,
+            "" + isAndroidBuildVersionAdded.orElse(builder.getMinApiLevel()));
       }
       return cr;
     } finally {
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineMethodProtectedTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineMethodProtectedTest.java
new file mode 100644
index 0000000..42ee1c9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineMethodProtectedTest.java
@@ -0,0 +1,157 @@
+// Copyright (c) 2022, 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.apimodel;
+
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForDefaultInstanceInitializer;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForMethod;
+import static org.junit.Assume.assumeFalse;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.testing.AndroidBuildVersion;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import java.lang.reflect.Method;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+// This is a reproduction of b/216136762.
+@RunWith(Parameterized.class)
+public class ApiModelOutlineMethodProtectedTest extends TestBase {
+
+  private static final AndroidApiLevel classApiLevel = AndroidApiLevel.M;
+  private static final AndroidApiLevel methodApiLevel = AndroidApiLevel.O_MR1;
+
+  private static final String TESTCLASS_DESCRIPTOR = "Lfoo/bar/Baz;";
+
+  @Parameter public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().build();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    // TODO(b/197078995): Make this work on 12.
+    assumeFalse(
+        parameters.isDexRuntime() && parameters.getDexRuntimeVersion().isEqualTo(Version.V12_0_0));
+    Method[] apiMethods =
+        new Method[] {
+          LibraryClass.class.getDeclaredMethod("addedOn27"),
+          LibraryClass.class.getDeclaredMethod("alsoAddedOn27"),
+          LibraryClass.class.getDeclaredMethod("superInvokeOn27")
+        };
+    AndroidApiLevel runApiLevel =
+        parameters.isCfRuntime()
+            ? AndroidApiLevel.B
+            : parameters.getRuntime().maxSupportedApiLevel();
+    boolean willInvokeLibraryMethods =
+        parameters.isDexRuntime() && runApiLevel.isGreaterThanOrEqualTo(methodApiLevel);
+    testForR8(parameters.getBackend())
+        .addLibraryClasses(LibraryClass.class)
+        .addLibraryFiles(
+            parameters.isCfRuntime()
+                ? ToolHelper.getJava8RuntimeJar()
+                : ToolHelper.getFirstSupportedAndroidJar(runApiLevel))
+        .addProgramClassFileData(
+            transformer(TestClass.class).setClassDescriptor(TESTCLASS_DESCRIPTOR).transform(),
+            transformer(Main.class)
+                .replaceClassDescriptorInMethodInstructions(
+                    descriptor(TestClass.class), TESTCLASS_DESCRIPTOR)
+                .transform())
+        .addKeepMainRule(Main.class)
+        .setMinApi(AndroidApiLevel.B)
+        .addAndroidBuildVersion(runApiLevel)
+        .apply(setMockApiLevelForClass(LibraryClass.class, classApiLevel))
+        .apply(setMockApiLevelForDefaultInstanceInitializer(LibraryClass.class, classApiLevel))
+        .apply(
+            builder -> {
+              for (Method apiMethod : apiMethods) {
+                setMockApiLevelForMethod(apiMethod, methodApiLevel).accept(builder);
+              }
+            })
+        .apply(ApiModelingTestHelper::enableOutliningOfMethods)
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .compile()
+        .applyIf(willInvokeLibraryMethods, b -> b.addBootClasspathClasses(LibraryClass.class))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLinesIf(!willInvokeLibraryMethods, "Not calling API")
+        .assertSuccessWithOutputLinesIf(
+            willInvokeLibraryMethods,
+            "Could not access LibraryClass::addedOn27",
+            "LibraryClass::addedOn27",
+            "LibraryClass::addedOn27",
+            "LibraryCLass::alsoAddedOn27",
+            "TestClass::superInvokeOn27",
+            "LibraryCLass::superInvokeOn27");
+  }
+
+  // Only present from api level 23.
+  public static class LibraryClass {
+
+    protected static void addedOn27() {
+      System.out.println("LibraryClass::addedOn27");
+    }
+
+    protected void alsoAddedOn27() {
+      System.out.println("LibraryCLass::alsoAddedOn27");
+    }
+
+    protected void superInvokeOn27() {
+      System.out.println("LibraryCLass::superInvokeOn27");
+    }
+  }
+
+  @NeverClassInline
+  public static class /* foo.bar */ TestClass extends LibraryClass {
+
+    @NeverInline
+    public static void callAddedOn27() {
+      LibraryClass.addedOn27();
+    }
+
+    @NeverInline
+    public void test() {
+      addedOn27();
+      LibraryClass libraryClass = this;
+      libraryClass.alsoAddedOn27();
+      libraryClass.superInvokeOn27();
+    }
+
+    @Override
+    @NeverInline
+    protected void superInvokeOn27() {
+      System.out.println("TestClass::superInvokeOn27");
+      super.superInvokeOn27();
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      if (AndroidBuildVersion.VERSION >= 27) {
+        try {
+          TestClass.addedOn27();
+        } catch (IllegalAccessError accessError) {
+          System.out.println("Could not access LibraryClass::addedOn27");
+        }
+        TestClass.callAddedOn27();
+        new TestClass().test();
+      } else {
+        System.out.println("Not calling API");
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/StatefulSingletonClassesMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/StatefulSingletonClassesMergingTest.java
index 3f887af..465cd5e 100644
--- a/src/test/java/com/android/tools/r8/classmerging/StatefulSingletonClassesMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/StatefulSingletonClassesMergingTest.java
@@ -40,7 +40,6 @@
         .enableInliningAnnotations()
         .enableMemberValuePropagationAnnotations()
         .enableNeverClassInliningAnnotations()
-        .noClassStaticizing()
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("A", "B");
diff --git a/src/test/java/com/android/tools/r8/classmerging/StatelessSingletonClassesMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/StatelessSingletonClassesMergingTest.java
index e6752f8..8e2433a 100644
--- a/src/test/java/com/android/tools/r8/classmerging/StatelessSingletonClassesMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/StatelessSingletonClassesMergingTest.java
@@ -36,7 +36,6 @@
             inspector -> inspector.assertIsCompleteMergeGroup(A.class, B.class))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
-        .noClassStaticizing()
         .setMinApi(parameters.getApiLevel())
         .compile()
         .run(parameters.getRuntime(), Main.class)
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingTestBase.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingTestBase.java
index fa19056..f941a29 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingTestBase.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingTestBase.java
@@ -7,9 +7,6 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
-import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
@@ -26,14 +23,4 @@
   public static TestParametersCollection data() {
     return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
-
-  protected ClassSubject getSynthesizedArgumentClassSubject(CodeInspector codeInspector) {
-    return codeInspector.allClasses().stream()
-        .filter(
-            clazz ->
-                SyntheticItemsTestUtils.isHorizontalInitializerTypeArgument(
-                    clazz.getOriginalReference()))
-        .findFirst()
-        .get();
-  }
 }
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/InheritInterfaceWithDefaultTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/InheritInterfaceWithDefaultTest.java
index ef0cedf..1f413bc 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/InheritInterfaceWithDefaultTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/InheritInterfaceWithDefaultTest.java
@@ -11,9 +11,6 @@
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.classmerging.horizontal.EmptyClassTest.A;
-import com.android.tools.r8.classmerging.horizontal.EmptyClassTest.B;
-import com.android.tools.r8.classmerging.horizontal.EmptyClassTest.Main;
 import org.junit.Test;
 
 public class InheritInterfaceWithDefaultTest extends HorizontalClassMergingTestBase {
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/PutObjectWithFinalizeTest.java b/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/PutObjectWithFinalizeTest.java
index 17e3f64..eb3138c 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/PutObjectWithFinalizeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/PutObjectWithFinalizeTest.java
@@ -45,8 +45,6 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(PutObjectWithFinalizeTest.class)
         .addKeepMainRule(TestClass.class)
-        // The class staticizer does not consider the finalize() method.
-        .addOptionsModification(options -> options.enableClassStaticizer = false)
         .enableInliningAnnotations()
         .enableNoHorizontalClassMergingAnnotations()
         .enableNoVerticalClassMergingAnnotations()
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
index 15f68fc..ad9ca05 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
@@ -170,14 +170,13 @@
     assertEquals(
         Lists.newArrayList(
             "STATIC: String SimpleWithParams.bar(String)",
-            "STATIC: String TrivialTestClass.next()",
-            "SimpleWithParams SimpleWithParams.INSTANCE",
-            "VIRTUAL: String SimpleWithParams.foo()"),
+            "STATIC: String SimpleWithParams.foo()",
+            "STATIC: String TrivialTestClass.next()"),
         references(clazz, "testSimpleWithParams", "void"));
 
     ClassSubject simpleWithParams = inspector.clazz(SimpleWithParams.class);
-    assertFalse(instanceMethods(simpleWithParams).isEmpty());
-    assertThat(simpleWithParams.clinit(), isPresent());
+    assertTrue(instanceMethods(simpleWithParams).isEmpty());
+    assertThat(simpleWithParams.clinit(), isAbsent());
 
     assertEquals(
         Lists.newArrayList(
@@ -198,21 +197,21 @@
             "STATIC: String TrivialTestClass.next()"),
         references(clazz, "testSimpleWithGetter", "void"));
 
+    // TODO(b/216254482): This can be optimized by pruning (always) simple caller inlined methods
+    //  after the primary optimization pass.
     ClassSubject simpleWithGetter = inspector.clazz(SimpleWithGetter.class);
-    assertTrue(instanceMethods(simpleWithGetter).isEmpty());
-    assertThat(simpleWithGetter.clinit(), not(isPresent()));
+    assertEquals(1, instanceMethods(simpleWithGetter).size());
+    assertThat(simpleWithGetter.clinit(), isPresent());
 
     assertEquals(
         Lists.newArrayList(
-            "STATIC: String SimpleWithThrowingGetter.bar(String)",
-            "STATIC: String TrivialTestClass.next()",
-            "SimpleWithThrowingGetter SimpleWithThrowingGetter.INSTANCE",
-            "VIRTUAL: String SimpleWithThrowingGetter.foo()"),
+            "STATIC: String SimpleWithLazyInit.bar$1(String)",
+            "STATIC: String SimpleWithLazyInit.foo$1()",
+            "STATIC: String TrivialTestClass.next()"),
         references(clazz, "testSimpleWithThrowingGetter", "void"));
 
     ClassSubject simpleWithThrowingGetter = inspector.clazz(SimpleWithThrowingGetter.class);
-    assertFalse(instanceMethods(simpleWithThrowingGetter).isEmpty());
-    assertThat(simpleWithThrowingGetter.clinit(), isPresent());
+    assertThat(simpleWithThrowingGetter, isAbsent());
 
     // TODO(b/143389508): add support for lazy init in singleton instance getter.
     assertEquals(
@@ -220,19 +219,19 @@
             "DIRECT: void SimpleWithLazyInit.<init>()",
             "DIRECT: void SimpleWithLazyInit.<init>()",
             "STATIC: String SimpleWithLazyInit.bar(String)",
+            "STATIC: String SimpleWithLazyInit.foo()",
             "STATIC: String TrivialTestClass.next()",
             "SimpleWithLazyInit SimpleWithLazyInit.INSTANCE",
             "SimpleWithLazyInit SimpleWithLazyInit.INSTANCE",
             "SimpleWithLazyInit SimpleWithLazyInit.INSTANCE",
             "SimpleWithLazyInit SimpleWithLazyInit.INSTANCE",
             "SimpleWithLazyInit SimpleWithLazyInit.INSTANCE",
-            "SimpleWithLazyInit SimpleWithLazyInit.INSTANCE",
-            "VIRTUAL: String SimpleWithLazyInit.foo()"),
+            "SimpleWithLazyInit SimpleWithLazyInit.INSTANCE"),
         references(clazz, "testSimpleWithLazyInit", "void"));
 
     ClassSubject simpleWithLazyInit = inspector.clazz(SimpleWithLazyInit.class);
     assertFalse(instanceMethods(simpleWithLazyInit).isEmpty());
-    assertThat(simpleWithLazyInit.clinit(), not(isPresent()));
+    assertThat(simpleWithLazyInit.clinit(), isPresent());
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/InvokeMethodWithNonNullParamCheckTest.java b/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/InvokeMethodWithNonNullParamCheckTest.java
index 828f4c0..35b475b 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/InvokeMethodWithNonNullParamCheckTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/InvokeMethodWithNonNullParamCheckTest.java
@@ -10,7 +10,8 @@
 
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
-import com.android.tools.r8.ToolHelper;
+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.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -22,6 +23,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
 import org.junit.runners.Parameterized.Parameters;
 
 /**
@@ -35,15 +37,12 @@
 @RunWith(Parameterized.class)
 public class InvokeMethodWithNonNullParamCheckTest extends TestBase {
 
-  private final Backend backend;
+  @Parameter(0)
+  public TestParameters parameters;
 
-  @Parameters(name = "Backend: {0}")
-  public static Backend[] data() {
-    return ToolHelper.getBackends();
-  }
-
-  public InvokeMethodWithNonNullParamCheckTest(Backend backend) {
-    this.backend = backend;
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
   @Test
@@ -72,7 +71,7 @@
     testForJvm().addTestClasspath().run(TestClass.class).assertSuccessWithOutput(expected);
 
     CodeInspector inspector =
-        testForR8(backend)
+        testForR8(parameters.getBackend())
             .addInnerClasses(InvokeMethodWithNonNullParamCheckTest.class)
             .addKeepMainRule(TestClass.class)
             .enableInliningAnnotations()
@@ -82,7 +81,8 @@
                   // CatchHandlers(new A()).
                   options.enableClassInlining = false;
                 })
-            .run(TestClass.class)
+            .setMinApi(parameters.getApiLevel())
+            .run(parameters.getRuntime(), TestClass.class)
             .assertSuccessWithOutput(expected)
             .inspector();
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/EffectivelyUnusedNullableArgumentTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/EffectivelyUnusedNullableArgumentTest.java
new file mode 100644
index 0000000..54d2071
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/EffectivelyUnusedNullableArgumentTest.java
@@ -0,0 +1,95 @@
+// Copyright (c) 2022, 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.unusedarguments;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethodWithName;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isStatic;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class EffectivelyUnusedNullableArgumentTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        // TODO(b/173398086): uniqueMethodWithName() does not work with argument changes.
+        .noMinification()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+
+              MethodSubject fooMethodSubject = aClassSubject.uniqueMethodWithName("foo");
+              assertThat(fooMethodSubject, isPresent());
+              assertEquals(1, fooMethodSubject.getProgramMethod().getParameters().size());
+              assertEquals(
+                  aClassSubject.getDexProgramClass().getType(),
+                  fooMethodSubject.getProgramMethod().getParameter(0));
+              assertThat(
+                  fooMethodSubject,
+                  invokesMethodWithName(
+                      canUseJavaUtilObjectsRequireNonNull(parameters)
+                          ? "requireNonNull"
+                          : "getClass"));
+
+              MethodSubject barMethodSubject = aClassSubject.uniqueMethodWithName("bar");
+              assertThat(barMethodSubject, isStatic());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertFailureWithErrorThatThrows(NullPointerException.class);
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      A a = System.currentTimeMillis() > 0 ? null : new A();
+      A.foo(a);
+    }
+  }
+
+  @NeverClassInline
+  static class A {
+
+    // This parameter cannot be removed, since the program needs to fail at the call to A.bar() when
+    // A.foo(null) is called.
+    @NeverInline
+    static void foo(A a) {
+      a.bar();
+    }
+
+    @NeverInline
+    void bar() {
+      System.out.println("A.bar()");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinClassStaticizerTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinClassStaticizerTest.java
index 1b96692..f503ce8 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinClassStaticizerTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinClassStaticizerTest.java
@@ -11,12 +11,12 @@
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.KotlinTestParameters;
+import com.android.tools.r8.R8TestBuilder;
 import com.android.tools.r8.R8TestRunResult;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
-import com.google.common.base.Predicates;
 import java.util.Collection;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -44,45 +44,18 @@
   public void testCompanionAndRegularObjects() throws Exception {
     assumeTrue("Only work with -allowaccessmodification", allowAccessModification);
     final String mainClassName = "class_staticizer.MainKt";
-
-    // Without class staticizer.
-    runTest("class_staticizer", mainClassName, true)
-        .inspect(
-            inspector -> {
-              assertThat(inspector.clazz("class_staticizer.Derived$Companion"), isPresent());
-
-              // The Util class is there, but its instance methods have been inlined.
-              ClassSubject utilClass = inspector.clazz("class_staticizer.Util");
-              assertThat(utilClass, isPresent());
-              assertTrue(
-                  utilClass.allMethods().stream()
-                      .filter(Predicates.not(FoundMethodSubject::isStatic))
-                      .allMatch(FoundMethodSubject::isInstanceInitializer));
-            });
-
-    // With class staticizer.
-    runTest("class_staticizer", mainClassName, false)
+    runTest("class_staticizer", mainClassName)
         .inspect(
             inspector -> {
               assertThat(inspector.clazz("class_staticizer.Regular$Companion"), not(isPresent()));
 
               ClassSubject utilClass = inspector.clazz("class_staticizer.Util");
               assertThat(utilClass, isPresent());
-              // TODO(b/179951488): The <init> is not removed in CF
-              if (testParameters.isDexRuntime()) {
-                assertTrue(utilClass.allMethods().stream().allMatch(FoundMethodSubject::isStatic));
-              }
+              assertTrue(utilClass.allMethods().stream().allMatch(FoundMethodSubject::isStatic));
             });
   }
 
-  protected R8TestRunResult runTest(String folder, String mainClass, boolean noClassStaticizing)
-      throws Exception {
-    return runTest(
-        folder,
-        mainClass,
-        testBuilder ->
-            testBuilder
-                .noClassInlining()
-                .noClassStaticizing(noClassStaticizing));
+  protected R8TestRunResult runTest(String folder, String mainClass) throws Exception {
+    return runTest(folder, mainClass, R8TestBuilder::noClassInlining);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java b/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java
index 50e9c6e..c53700b 100644
--- a/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java
@@ -73,22 +73,6 @@
   }
 
   @Test
-  public void testDontShrinkAndDontOptimizeDifferently() throws Exception {
-    test(
-        builder ->
-            builder
-                .addKeepRules("-keep,allowobfuscation class **.*KClasses*")
-                .noTreeShaking()
-                .addOptionsModification(
-                    o -> {
-                      // Randomly choose a couple of optimizations.
-                      o.enableVerticalClassMerging = false;
-                      o.enableClassStaticizer = false;
-                      o.outline.enabled = false;
-                    }));
-  }
-
-  @Test
   public void testDontShrinkAndDontObfuscate() throws Exception {
     test(builder -> builder.noMinification().noTreeShaking());
   }
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java
index b3af78a..907ca27 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java
@@ -11,7 +11,6 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.KotlinTestParameters;
-import com.android.tools.r8.R8TestBuilder;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
@@ -87,7 +86,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionPropertiesKt",
         "companionProperties_usePrimitiveProp");
-    runTest(PROPERTIES_PACKAGE_NAME, mainClass, R8TestBuilder::noClassStaticizing)
+    runTest(PROPERTIES_PACKAGE_NAME, mainClass)
         .inspect(
             inspector -> {
               if (allowAccessModification) {
@@ -107,9 +106,9 @@
               MemberNaming.MethodSignature setterAccessor =
                   testedClass.getSetterAccessorForProperty(
                       propertyName, AccessorKind.FROM_COMPANION);
-                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-                checkMethodIsKept(outerClass, getterAccessor);
-                checkMethodIsRemoved(outerClass, setterAccessor);
+              assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+              checkMethodIsKept(outerClass, getterAccessor);
+              checkMethodIsRemoved(outerClass, setterAccessor);
             });
   }
 
@@ -118,7 +117,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionPropertiesKt",
         "companionProperties_usePrivateProp");
-    runTest(PROPERTIES_PACKAGE_NAME, mainClass, R8TestBuilder::noClassStaticizing)
+    runTest(PROPERTIES_PACKAGE_NAME, mainClass)
         .inspect(
             inspector -> {
               if (allowAccessModification) {
@@ -139,10 +138,10 @@
               MemberNaming.MethodSignature setterAccessor =
                   testedClass.getSetterAccessorForProperty(
                       propertyName, AccessorKind.FROM_COMPANION);
-                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+              assertTrue(fieldSubject.getField().accessFlags.isPrivate());
 
-                checkMethodIsKept(outerClass, getterAccessor);
-                checkMethodIsRemoved(outerClass, setterAccessor);
+              checkMethodIsKept(outerClass, getterAccessor);
+              checkMethodIsRemoved(outerClass, setterAccessor);
             });
   }
 
@@ -151,7 +150,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionPropertiesKt",
         "companionProperties_useInternalProp");
-    runTest(PROPERTIES_PACKAGE_NAME, mainClass, R8TestBuilder::noClassStaticizing)
+    runTest(PROPERTIES_PACKAGE_NAME, mainClass)
         .inspect(
             inspector -> {
               if (allowAccessModification) {
@@ -184,7 +183,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionPropertiesKt",
         "companionProperties_usePublicProp");
-    runTest(PROPERTIES_PACKAGE_NAME, mainClass, R8TestBuilder::noClassStaticizing)
+    runTest(PROPERTIES_PACKAGE_NAME, mainClass)
         .inspect(
             inspector -> {
               if (allowAccessModification) {
@@ -206,9 +205,9 @@
                   testedClass.getSetterAccessorForProperty(
                       propertyName, AccessorKind.FROM_COMPANION);
 
-                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-                checkMethodIsKept(outerClass, getterAccessor);
-                checkMethodIsRemoved(outerClass, setterAccessor);
+              assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+              checkMethodIsKept(outerClass, getterAccessor);
+              checkMethodIsRemoved(outerClass, setterAccessor);
             });
   }
 
@@ -217,7 +216,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_usePrivateLateInitProp");
-    runTest(PROPERTIES_PACKAGE_NAME, mainClass, R8TestBuilder::noClassStaticizing)
+    runTest(PROPERTIES_PACKAGE_NAME, mainClass)
         .inspect(
             inspector -> {
               if (allowAccessModification) {
@@ -270,10 +269,7 @@
     runTest(
             "accessors",
             mainClass,
-            builder -> {
-              builder.addClasspathFiles(kotlinc.getKotlinAnnotationJar());
-              builder.noClassStaticizing();
-            })
+            builder -> builder.addClasspathFiles(kotlinc.getKotlinAnnotationJar()))
         .inspect(
             inspector -> {
               // The classes are removed entirely as a result of member value propagation, inlining,
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
index c979be9..59ba99b 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
@@ -91,7 +91,6 @@
       o -> {
         o.enableClassInlining = false;
         o.enableVerticalClassMerging = false;
-        o.enableClassStaticizer = false;
       };
 
   @Parameterized.Parameters(name = "{0}, {1}, allowAccessModification: {2}")
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/RetraceInlineBranchTest.java b/src/test/java/com/android/tools/r8/naming/retrace/RetraceInlineBranchTest.java
new file mode 100644
index 0000000..92b6019
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/retrace/RetraceInlineBranchTest.java
@@ -0,0 +1,157 @@
+// Copyright (c) 2022, 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.naming.retrace;
+
+import static com.android.tools.r8.naming.retrace.StackTrace.isSame;
+import static com.android.tools.r8.naming.retrace.StackTrace.isSameExceptForLineNumbers;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.naming.retrace.StackTrace.StackTraceLine;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class RetraceInlineBranchTest extends TestBase {
+
+  @Parameter() public TestParameters parameters;
+
+  public StackTrace expectedStackTrace;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Before
+  public void setup() throws Exception {
+    // Get the expected stack trace by running on the JVM.
+    expectedStackTrace =
+        testForRuntime(parameters)
+            .addInnerClasses(getClass())
+            .run(parameters.getRuntime(), Main.class)
+            .getStackTrace();
+  }
+
+  @Test
+  public void testR8() throws Throwable {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .addKeepAttributeLineNumberTable()
+        .enableNeverClassInliningAnnotations()
+        .enableInliningAnnotations()
+        .run(parameters.getRuntime(), Main.class)
+        .inspectStackTrace(
+            (stackTrace, inspector) -> {
+              if (canUseJavaUtilObjectsRequireNonNull(parameters)) {
+                StackTrace requireNonNullFrame =
+                    StackTrace.builder().add(stackTrace.get(0)).build();
+                assertThat(
+                    requireNonNullFrame,
+                    isSameExceptForLineNumbers(
+                        StackTrace.builder()
+                            .add(
+                                StackTraceLine.builder()
+                                    .setClassName(Objects.class.getTypeName())
+                                    .setMethodName("requireNonNull")
+                                    .setFileName("Objects.java")
+                                    .build())
+                            .build()));
+
+                StackTrace stackTraceWithoutRequireNonNull =
+                    StackTrace.builder().add(stackTrace).remove(0).build();
+                assertThat(stackTraceWithoutRequireNonNull, isSame(expectedStackTrace));
+              } else {
+                // To check that we inserted the check in the branch correctly we use a rudimentary
+                // line check by comparing that the getClass method comes later than the first if.
+                MethodSubject methodSubject =
+                    inspector.clazz(Main.class).uniqueMethodWithName("call");
+                assertThat(methodSubject, isPresent());
+                List<InstructionSubject> getClassCalls =
+                    methodSubject
+                        .streamInstructions()
+                        .filter(
+                            instructionSubject ->
+                                instructionSubject.isInvoke()
+                                    && instructionSubject
+                                        .getMethod()
+                                        .qualifiedName()
+                                        .contains("getClass"))
+                        .collect(Collectors.toList());
+                assertEquals(1, getClassCalls.size());
+                Optional<InstructionSubject> firstIf =
+                    methodSubject.streamInstructions().filter(InstructionSubject::isIf).findFirst();
+                assertTrue(firstIf.isPresent());
+                assertTrue(
+                    methodSubject.getLineNumberForInstruction(getClassCalls.get(0))
+                        > methodSubject.getLineNumberForInstruction(firstIf.get()));
+                assertThat(stackTrace, isSame(expectedStackTrace));
+              }
+            });
+  }
+
+  @NeverClassInline
+  static class Foo {
+
+    private int value;
+
+    @NeverInline
+    int getValue() {
+      return value;
+    }
+
+    @NeverInline
+    void setValue(int value) {
+      this.value = value;
+    }
+
+    void inlinable(int arg) {
+      String newValue;
+      if (arg > 0) {
+        newValue = getValue() + "";
+      } else {
+        if (arg < 0) {
+          setValue(arg);
+          newValue = "-1";
+        } else {
+          // When inlining this block this is the only path that needs a null-check.
+          newValue = "Hello World";
+        }
+      }
+      System.out.println(newValue);
+    }
+  }
+
+  static class Main {
+
+    @NeverInline
+    private static void call(Foo foo, String[] args) {
+      foo.inlinable(args.length);
+    }
+
+    public static void main(String[] args) {
+      call(args.length == 0 ? null : new Foo(), args);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/RetraceInlineConditionTest.java b/src/test/java/com/android/tools/r8/naming/retrace/RetraceInlineConditionTest.java
index 15466f7..36921ce 100644
--- a/src/test/java/com/android/tools/r8/naming/retrace/RetraceInlineConditionTest.java
+++ b/src/test/java/com/android/tools/r8/naming/retrace/RetraceInlineConditionTest.java
@@ -4,13 +4,16 @@
 
 package com.android.tools.r8.naming.retrace;
 
+import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 
-import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestBuilder;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import org.hamcrest.CoreMatchers;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -22,25 +25,39 @@
 
   @Parameter() public TestParameters parameters;
 
+  public StackTrace expectedStackTrace;
+
   @Parameters(name = "{0}")
   public static TestParametersCollection data() {
     return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
+  @Before
+  public void setup() throws Exception {
+    // Get the expected stack trace by running on the JVM.
+    TestBuilder<? extends SingleTestRunResult<?>, ?> testBuilder =
+        testForRuntime(parameters).addInnerClasses(getClass());
+    SingleTestRunResult<?> runResult = testBuilder.run(parameters.getRuntime(), Main.class);
+    if (parameters.isCfRuntime()) {
+      expectedStackTrace = runResult.map(StackTrace::extractFromJvm);
+    } else {
+      expectedStackTrace =
+          StackTrace.extractFromArt(runResult.getStdErr(), parameters.asDexRuntime().getVm());
+    }
+  }
+
   @Test
   public void testR8() throws Throwable {
-    R8TestCompileResult compileResult =
-        testForR8(parameters.getBackend())
-            .addInnerClasses(getClass())
-            .setMinApi(parameters.getApiLevel())
-            .addKeepMainRule(Main.class)
-            .compile()
-            .inspectProguardMap(
-                map -> {
-                  // TODO(b/215339687): We should not have a rewriteFrame in the mapping file since
-                  //  an explicit null check should be inserted.
-                  assertThat(map, CoreMatchers.containsString("com.android.tools.r8.rewriteFrame"));
-                });
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .compile()
+        .inspectProguardMap(
+            map -> {
+              assertThat(
+                  map, not(CoreMatchers.containsString("com.android.tools.r8.rewriteFrame")));
+            });
   }
 
   static class Foo {
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/RetraceInlineeWithNullCheckFollowingImplicitReceiverNullCheckTest.java b/src/test/java/com/android/tools/r8/naming/retrace/RetraceInlineeWithNullCheckFollowingImplicitReceiverNullCheckTest.java
index b5cd101..15f867f 100644
--- a/src/test/java/com/android/tools/r8/naming/retrace/RetraceInlineeWithNullCheckFollowingImplicitReceiverNullCheckTest.java
+++ b/src/test/java/com/android/tools/r8/naming/retrace/RetraceInlineeWithNullCheckFollowingImplicitReceiverNullCheckTest.java
@@ -31,12 +31,10 @@
   }
 
   public StackTrace expectedStackTrace;
-  public StackTrace unexpectedStackTrace;
 
   @Before
   public void setup() throws Exception {
     expectedStackTrace = getStackTrace();
-    unexpectedStackTrace = getStackTrace("foo");
   }
 
   private StackTrace getStackTrace(String... args) throws Exception {
@@ -62,9 +60,8 @@
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), Caller.class)
         .assertFailureWithErrorThatThrows(NullPointerException.class)
-        // TODO(b/214377135): Should retrace to expectedStackTrace.
         .inspectStackTrace(
-            (stackTrace, codeInspector) -> assertThat(stackTrace, isSame(unexpectedStackTrace)));
+            (stackTrace, codeInspector) -> assertThat(stackTrace, isSame(expectedStackTrace)));
   }
 
   static class Foo {
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/RetraceInlineeWithNullCheckInlinedTest.java b/src/test/java/com/android/tools/r8/naming/retrace/RetraceInlineeWithNullCheckInlinedTest.java
index 6143522..6b7b2d0 100644
--- a/src/test/java/com/android/tools/r8/naming/retrace/RetraceInlineeWithNullCheckInlinedTest.java
+++ b/src/test/java/com/android/tools/r8/naming/retrace/RetraceInlineeWithNullCheckInlinedTest.java
@@ -4,19 +4,21 @@
 package com.android.tools.r8.naming.retrace;
 
 import static com.android.tools.r8.naming.retrace.StackTrace.isSame;
+import static com.android.tools.r8.naming.retrace.StackTrace.isSameExceptForLineNumbers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
-import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NoMethodStaticizing;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.naming.retrace.StackTrace.StackTraceLine;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import java.util.List;
+import java.util.Objects;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -71,8 +73,28 @@
               MethodSubject staticized = callerClass.uniqueMethodWithName("outerCaller");
               assertThat(staticized, isPresentAndRenamed());
               // TODO(b/214377135): The stack traces should be the same (when 206427323) is
-              // resolved.
-              assertThat(stackTrace, notIf(isSame(expectedStackTrace), throwReceiverNpe));
+              //  resolved.
+              if (throwReceiverNpe && canUseJavaUtilObjectsRequireNonNull(parameters)) {
+                StackTrace requireNonNullFrame =
+                    StackTrace.builder().add(stackTrace.get(0)).build();
+                assertThat(
+                    requireNonNullFrame,
+                    isSameExceptForLineNumbers(
+                        StackTrace.builder()
+                            .add(
+                                StackTraceLine.builder()
+                                    .setClassName(Objects.class.getTypeName())
+                                    .setMethodName("requireNonNull")
+                                    .setFileName("Objects.java")
+                                    .build())
+                            .build()));
+
+                StackTrace stackTraceWithoutRequireNonNull =
+                    StackTrace.builder().add(stackTrace).remove(0).build();
+                assertThat(stackTraceWithoutRequireNonNull, isSame(expectedStackTrace));
+              } else {
+                assertThat(stackTrace, isSame(expectedStackTrace));
+              }
             });
   }
 
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/RetraceInlineeWithNullCheckTest.java b/src/test/java/com/android/tools/r8/naming/retrace/RetraceInlineeWithNullCheckTest.java
index 13e3e7f..70c2be4 100644
--- a/src/test/java/com/android/tools/r8/naming/retrace/RetraceInlineeWithNullCheckTest.java
+++ b/src/test/java/com/android/tools/r8/naming/retrace/RetraceInlineeWithNullCheckTest.java
@@ -8,11 +8,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.NeverInline;
-import com.android.tools.r8.R8FullTestBuilder;
-import com.android.tools.r8.R8TestRunResult;
-import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.TestBase;
-import com.android.tools.r8.TestBuilder;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.naming.retrace.StackTrace.StackTraceLine;
 import com.android.tools.r8.utils.BooleanUtils;
@@ -43,46 +39,36 @@
 
   public StackTrace expectedStackTrace;
 
+  private String[] getArgs() {
+    return throwReceiverNpe ? new String[] {"foo"} : new String[0];
+  }
+
   @Before
   public void setup() throws Exception {
-    // Get the expected stack trace by running on the JVM.
-    TestBuilder<? extends SingleTestRunResult<?>, ?> testBuilder =
-        testForRuntime(parameters).addProgramClasses(Caller.class, Foo.class);
-    SingleTestRunResult<?> runResult;
-    if (throwReceiverNpe) {
-      runResult = testBuilder.run(parameters.getRuntime(), Caller.class, "foo");
-    } else {
-      runResult = testBuilder.run(parameters.getRuntime(), Caller.class);
-    }
-    if (parameters.isCfRuntime()) {
-      expectedStackTrace = runResult.map(StackTrace::extractFromJvm);
-    } else {
-      expectedStackTrace =
-          StackTrace.extractFromArt(runResult.getStdErr(), parameters.asDexRuntime().getVm());
-    }
+    // Get the expected stack trace by running on the JVM/ART.
+    expectedStackTrace =
+        testForRuntime(parameters)
+            .addProgramClasses(Caller.class, Foo.class)
+            .run(parameters.getRuntime(), Caller.class, getArgs())
+            .getStackTrace();
   }
 
   @Test
   public void testR8() throws Exception {
-    R8FullTestBuilder r8FullTestBuilder =
-        testForR8(parameters.getBackend())
-            .addInnerClasses(getClass())
-            .addKeepMainRule(Caller.class)
-            .addKeepAttributeLineNumberTable()
-            .addKeepAttributeSourceFile()
-            .setMinApi(parameters.getApiLevel())
-            .enableInliningAnnotations()
-            .enableExperimentalMapFileVersion();
-    R8TestRunResult runResult;
-    if (throwReceiverNpe) {
-      runResult = r8FullTestBuilder.run(parameters.getRuntime(), Caller.class, "foo");
-    } else {
-      runResult = r8FullTestBuilder.run(parameters.getRuntime(), Caller.class);
-    }
-    runResult
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Caller.class)
+        .addKeepAttributeLineNumberTable()
+        .addKeepAttributeSourceFile()
+        .setMinApi(parameters.getApiLevel())
+        .enableInliningAnnotations()
+        .enableExperimentalMapFileVersion()
+        .run(parameters.getRuntime(), Caller.class, getArgs())
         .assertFailureWithErrorThatThrows(NullPointerException.class)
         .inspectStackTrace(
             (stackTrace, codeInspector) -> {
+              // TODO(b/214377135): The stack traces should be the same (when 206427323) is
+              //  resolved.
               if (throwReceiverNpe && canUseJavaUtilObjectsRequireNonNull(parameters)) {
                 StackTrace requireNonNullFrame =
                     StackTrace.builder().add(stackTrace.get(0)).build();
diff --git a/src/test/java/com/android/tools/r8/naming/retraceproguard/RetraceTestBase.java b/src/test/java/com/android/tools/r8/naming/retraceproguard/RetraceTestBase.java
index 2200ea7..10fec23 100644
--- a/src/test/java/com/android/tools/r8/naming/retraceproguard/RetraceTestBase.java
+++ b/src/test/java/com/android/tools/r8/naming/retraceproguard/RetraceTestBase.java
@@ -6,9 +6,11 @@
 
 import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.R8TestBuilder;
+import com.android.tools.r8.R8TestCompileResult;
 import com.android.tools.r8.R8TestRunResult;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ThrowableConsumer;
 import com.android.tools.r8.naming.retraceproguard.StackTrace.StackTraceLine;
 import com.google.common.collect.ImmutableList;
 import java.util.Collection;
@@ -51,6 +53,14 @@
 
   public void runTest(List<String> keepRules, BiConsumer<StackTrace, StackTrace> checker)
       throws Exception {
+    runTest(keepRules, checker, ThrowableConsumer.empty());
+  }
+
+  public void runTest(
+      List<String> keepRules,
+      BiConsumer<StackTrace, StackTrace> checker,
+      ThrowableConsumer<R8TestCompileResult> compileResultConsumer)
+      throws Exception {
     R8TestRunResult result =
         (compat ? testForR8Compat(parameters.getBackend()) : testForR8(parameters.getBackend()))
             .setMode(mode)
@@ -60,6 +70,8 @@
             .addKeepRules(keepRules)
             .setMinApi(parameters.getApiLevel())
             .apply(this::configure)
+            .compile()
+            .apply(compileResultConsumer)
             .run(parameters.getRuntime(), getMainClass())
             .assertFailure();
 
diff --git a/src/test/java/com/android/tools/r8/naming/retraceproguard/VerticalClassMergingRetraceTest.java b/src/test/java/com/android/tools/r8/naming/retraceproguard/VerticalClassMergingRetraceTest.java
index 5905930..bb37a8a 100644
--- a/src/test/java/com/android/tools/r8/naming/retraceproguard/VerticalClassMergingRetraceTest.java
+++ b/src/test/java/com/android/tools/r8/naming/retraceproguard/VerticalClassMergingRetraceTest.java
@@ -5,6 +5,8 @@
 
 import static com.android.tools.r8.naming.retraceproguard.StackTrace.isSameExceptForFileName;
 import static com.android.tools.r8.naming.retraceproguard.StackTrace.isSameExceptForFileNameAndLineNumber;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.onlyIf;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assume.assumeTrue;
@@ -12,11 +14,16 @@
 import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.R8TestBuilder;
+import com.android.tools.r8.R8TestCompileResult;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.naming.retraceproguard.StackTrace.StackTraceLine;
 import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import com.google.common.collect.ImmutableList;
+import java.io.IOException;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Set;
@@ -75,45 +82,54 @@
     return height;
   }
 
-  private boolean filterSynthesizedMethod(StackTraceLine retracedStackTraceLine) {
-    return retracedStackTraceLine.lineNumber > 0;
+  private boolean filterSynthesizedMethod(
+      StackTraceLine retracedStackTraceLine, MethodSubject syntheticMethod) {
+    if (syntheticMethod.isPresent()) {
+      String qualifiedMethodName =
+          retracedStackTraceLine.className + "." + retracedStackTraceLine.methodName;
+      return !qualifiedMethodName.equals(syntheticMethod.getOriginalName())
+          || retracedStackTraceLine.lineNumber > 0;
+    }
+    return true;
   }
 
   @Test
   public void testSourceFileAndLineNumberTable() throws Exception {
+    Box<MethodSubject> syntheticMethod = new Box<>();
     runTest(
         ImmutableList.of("-keepattributes SourceFile,LineNumberTable"),
         (StackTrace actualStackTrace, StackTrace retracedStackTrace) -> {
           // Even when SourceFile is present retrace replaces the file name in the stack trace.
           StackTrace reprocessedStackTrace =
-              mode == CompilationMode.DEBUG
-                  ? retracedStackTrace
-                  : retracedStackTrace.filter(this::filterSynthesizedMethod);
+              retracedStackTrace.filter(
+                  stackTraceLine -> filterSynthesizedMethod(stackTraceLine, syntheticMethod.get()));
           assertThat(
               reprocessedStackTrace.filter(this::isNotDalvikNativeStartMethod),
               isSameExceptForFileName(
                   expectedStackTrace.filter(this::isNotDalvikNativeStartMethod)));
           assertEquals(expectedActualStackTraceHeight(), actualStackTrace.size());
-        });
+        },
+        compileResult -> setSyntheticMethod(compileResult, syntheticMethod));
   }
 
   @Test
   public void testLineNumberTableOnly() throws Exception {
     assumeTrue(compat);
     assumeTrue(parameters.isDexRuntime());
+    Box<MethodSubject> syntheticMethod = new Box<>();
     runTest(
         ImmutableList.of("-keepattributes LineNumberTable"),
         (StackTrace actualStackTrace, StackTrace retracedStackTrace) -> {
           StackTrace reprocessedStackTrace =
-              mode == CompilationMode.DEBUG
-                  ? retracedStackTrace
-                  : retracedStackTrace.filter(this::filterSynthesizedMethod);
+              retracedStackTrace.filter(
+                  stackTraceLine -> filterSynthesizedMethod(stackTraceLine, syntheticMethod.get()));
           assertThat(
               reprocessedStackTrace.filter(this::isNotDalvikNativeStartMethod),
               isSameExceptForFileName(
                   expectedStackTrace.filter(this::isNotDalvikNativeStartMethod)));
           assertEquals(expectedActualStackTraceHeight(), actualStackTrace.size());
-        });
+        },
+        compileResult -> setSyntheticMethod(compileResult, syntheticMethod));
   }
 
   @Test
@@ -121,18 +137,32 @@
     assumeTrue(compat);
     assumeTrue(parameters.isDexRuntime());
     haveSeenLines.clear();
+    Box<MethodSubject> syntheticMethod = new Box<>();
     runTest(
         ImmutableList.of(),
         (StackTrace actualStackTrace, StackTrace retracedStackTrace) -> {
           StackTrace reprocessedStackTrace =
-              mode == CompilationMode.DEBUG
-                  ? retracedStackTrace
-                  : retracedStackTrace.filter(this::filterSynthesizedMethod);
+              retracedStackTrace.filter(
+                  stackTraceLine -> filterSynthesizedMethod(stackTraceLine, syntheticMethod.get()));
           assertThat(
               reprocessedStackTrace.filter(this::isNotDalvikNativeStartMethod),
               isSameExceptForFileNameAndLineNumber(
                   expectedStackTrace.filter(this::isNotDalvikNativeStartMethod)));
           assertEquals(expectedActualStackTraceHeight(), actualStackTrace.size());
+        },
+        compileResult -> setSyntheticMethod(compileResult, syntheticMethod));
+  }
+
+  private void setSyntheticMethod(
+      R8TestCompileResult compileResult, Box<MethodSubject> syntheticMethod) throws IOException {
+    compileResult.inspect(
+        inspector -> {
+          ClassSubject tintResourcesClassSubject = inspector.clazz(TintResources.class);
+          MethodSubject uniqueSyntheticMethod =
+              tintResourcesClassSubject.uniqueMethodThatMatches(
+                  method -> method.getAccessFlags().isSynthetic());
+          assertThat(uniqueSyntheticMethod, onlyIf(mode == CompilationMode.RELEASE, isPresent()));
+          syntheticMethod.set(uniqueSyntheticMethod);
         });
   }
 }
diff --git a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/CompanionConstructorShakingTest.java b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/CompanionConstructorShakingTest.java
index f5fb4f5..828a4b5 100644
--- a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/CompanionConstructorShakingTest.java
+++ b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/CompanionConstructorShakingTest.java
@@ -4,9 +4,9 @@
 
 package com.android.tools.r8.optimize.argumentpropagation;
 
-import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isStatic;
+import static com.android.tools.r8.utils.codeinspector.Matchers.onlyIf;
 import static junit.framework.TestCase.assertEquals;
 import static org.hamcrest.MatcherAssert.assertThat;
 
@@ -16,6 +16,7 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import org.junit.Test;
@@ -40,7 +41,6 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
-        .addOptionsModification(options -> options.enableClassStaticizer = false)
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoHorizontalClassMergingAnnotations()
@@ -48,12 +48,19 @@
         .compile()
         .inspect(
             inspector -> {
+              boolean isCf = parameters.isCfRuntime();
+
               ClassSubject hostClassSubject = inspector.clazz(Host.class);
-              assertThat(hostClassSubject, isAbsent());
+              assertThat(hostClassSubject, isPresent());
+              assertEquals(1 + BooleanUtils.intValue(isCf), hostClassSubject.allMethods().size());
+              assertThat(hostClassSubject.clinit(), onlyIf(isCf, isPresent()));
+              assertThat(hostClassSubject.uniqueMethodWithName("keepHost"), isPresent());
 
               ClassSubject companionClassSubject = inspector.clazz(Host.Companion.class);
               assertThat(companionClassSubject, isPresent());
-              assertEquals(1, companionClassSubject.allMethods().size());
+              assertEquals(
+                  1 + BooleanUtils.intValue(isCf), companionClassSubject.allMethods().size());
+              assertThat(companionClassSubject.init(), onlyIf(isCf, isPresent()));
 
               MethodSubject greetMethodSubject =
                   companionClassSubject.uniqueMethodWithName("greet");
@@ -67,6 +74,7 @@
 
     public static void main(String[] args) {
       Host.companion.greet();
+      Host.keepHost();
     }
   }
 
@@ -74,13 +82,18 @@
 
     static final Companion companion = new Companion();
 
+    @NeverInline
+    static void keepHost() {
+      System.out.println();
+    }
+
     @NeverClassInline
     @NoHorizontalClassMerging
     static class Companion {
 
       @NeverInline
       void greet() {
-        System.out.println("Hello world!");
+        System.out.print("Hello world!");
       }
     }
   }
diff --git a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/EffectivelyFinalCompanionMethodsTest.java b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/EffectivelyFinalCompanionMethodsTest.java
new file mode 100644
index 0000000..67af262
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/EffectivelyFinalCompanionMethodsTest.java
@@ -0,0 +1,107 @@
+// Copyright (c) 2022, 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.argumentpropagation;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isStatic;
+import static junit.framework.TestCase.assertEquals;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class EffectivelyFinalCompanionMethodsTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .noMinification()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject hostClassSubject = inspector.clazz(Host.class);
+              assertThat(hostClassSubject, isAbsent());
+
+              ClassSubject companionClassSubject = inspector.clazz(Host.Companion.class);
+              assertThat(companionClassSubject, isPresent());
+              assertEquals(4, companionClassSubject.allMethods().size());
+              assertThat(companionClassSubject.uniqueMethodWithName("foo"), isStatic());
+              assertThat(companionClassSubject.uniqueMethodWithName("bar"), isStatic());
+              assertThat(companionClassSubject.uniqueMethodWithName("baz"), isStatic());
+              assertThat(companionClassSubject.uniqueMethodWithName("qux"), isStatic());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Foo!", "Bar!", "Baz!", "Qux!", "Baz!");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      Host.companion.foo();
+    }
+  }
+
+  static class Host {
+
+    static final Companion companion = new Companion();
+
+    @NeverClassInline
+    @NoHorizontalClassMerging
+    static class Companion {
+
+      @NeverInline
+      void foo() {
+        System.out.println("Foo!");
+        bar();
+      }
+
+      @NeverInline
+      void bar() {
+        System.out.println("Bar!");
+        baz(true);
+      }
+
+      @NeverInline
+      void baz(boolean doQux) {
+        System.out.println("Baz!");
+        if (doQux) {
+          qux();
+        }
+      }
+
+      @NeverInline
+      void qux() {
+        System.out.println("Qux!");
+        baz(false);
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/NullableCompanionConstructorShakingTest.java b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/NullableCompanionConstructorShakingTest.java
new file mode 100644
index 0000000..594f86f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/NullableCompanionConstructorShakingTest.java
@@ -0,0 +1,96 @@
+// Copyright (c) 2022, 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.argumentpropagation;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod;
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethodWithName;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isStatic;
+import static junit.framework.TestCase.assertEquals;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class NullableCompanionConstructorShakingTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(options -> options.enableClassStaticizer = false)
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              MethodSubject companionMethodSubject =
+                  inspector.clazz(Host.Companion.class).uniqueMethodWithName("greet");
+              assertThat(companionMethodSubject, isStatic());
+
+              MethodSubject mainMethodSubject = inspector.clazz(Main.class).mainMethod();
+              assertEquals(
+                  2,
+                  mainMethodSubject
+                      .streamInstructions()
+                      .filter(InstructionSubject::isInvoke)
+                      .count());
+              assertThat(
+                  mainMethodSubject,
+                  invokesMethodWithName(
+                      canUseJavaUtilObjectsRequireNonNull(parameters)
+                          ? "requireNonNull"
+                          : "getClass"));
+              assertThat(mainMethodSubject, invokesMethod(companionMethodSubject));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertFailureWithErrorThatThrows(NullPointerException.class);
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      // This method call will be staticized and a null check should be inserted for Host.companion.
+      Host.companion.greet();
+    }
+  }
+
+  static class Host {
+
+    static final Companion companion = System.currentTimeMillis() >= 0 ? null : new Companion();
+
+    @NeverClassInline
+    @NoHorizontalClassMerging
+    static class Companion {
+
+      @NeverInline
+      void greet() {
+        System.out.print("Hello world!");
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/b136698023/LibraryBridgeTest.java b/src/test/java/com/android/tools/r8/shaking/b136698023/LibraryBridgeTest.java
index 2ea5e50..baba02c 100644
--- a/src/test/java/com/android/tools/r8/shaking/b136698023/LibraryBridgeTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/b136698023/LibraryBridgeTest.java
@@ -87,7 +87,7 @@
 
   @Parameters(name = "{0}")
   public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().build();
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
   public LibraryBridgeTest(TestParameters parameters) {
@@ -102,7 +102,7 @@
         .addKeepClassAndMembersRules(ICloneable.class)
         .addKeepMainRule(MainWithOrdinaryClone.class)
         .noMinification()
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), MainWithOrdinaryClone.class)
         .assertSuccessWithOutputThatMatches(containsString(BaseOrdinaryClone.class.getTypeName()));
   }
@@ -115,7 +115,7 @@
         .addKeepClassAndMembersRules(XCloneable.class, Base.class)
         .addKeepMainRule(Main.class)
         .noMinification()
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputThatMatches(containsString(Model.class.getTypeName()));
   }
@@ -128,7 +128,7 @@
         .addKeepClassAndMembersRules(XCloneable.class, Base.class)
         .addKeepMainRule(MainImpl.class)
         .noMinification()
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), MainImpl.class)
         .assertSuccessWithOutputThatMatches(containsString(Model.class.getTypeName()));
   }
@@ -140,15 +140,15 @@
         testForR8(parameters.getBackend())
             .addProgramClasses(Base.class)
             .addKeepAllClassesRule()
-            .setMinApi(parameters.getRuntime())
+            .setMinApi(parameters.getApiLevel())
             .compile();
     testForR8(parameters.getBackend())
         .addProgramClasses(XCloneable.class, Model.class, Main.class)
         .addLibraryClasses(Base.class)
-        .addLibraryFiles(runtimeJar(parameters.getBackend()))
+        .addDefaultRuntimeLibrary(parameters)
         .addKeepClassAndMembersRules(XCloneable.class)
         .addKeepMainRule(Main.class)
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
         .noMinification()
         .compile()
         .addRunClasspathFiles(library.writeToZip())
diff --git a/src/test/java/com/android/tools/r8/shaking/proxy/ProxiesTest.java b/src/test/java/com/android/tools/r8/shaking/proxy/ProxiesTest.java
index e7bb456..047a109 100644
--- a/src/test/java/com/android/tools/r8/shaking/proxy/ProxiesTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/proxy/ProxiesTest.java
@@ -65,7 +65,6 @@
         .addOptionsModification(
             o -> {
               o.enableDevirtualization = false;
-              o.inlinerOptions().enableInliningOfInvokesWithNullableReceivers = false;
             })
         .enableAlwaysInliningAnnotations()
         .noMinification()
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java
index 6803e16..594b17a 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java
@@ -89,9 +89,9 @@
     return Streams.stream(iterateTryCatches());
   }
 
-  public void getLineNumberForInstruction(InstructionSubject subject) {
+  public int getLineNumberForInstruction(InstructionSubject subject) {
     assert hasLineNumberTable();
-    getLineNumberTable().getLineForInstruction(subject);
+    return getLineNumberTable().getLineForInstruction(subject);
   }
 
   @Override