Enforce calls to lookup virtual methods do so on valid resolutions.

Bug: 140016938
Bug: 140088797
Change-Id: Iea4d8e898bd41624b078aaf08ee05bd0ff06e742
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfo.java b/src/main/java/com/android/tools/r8/graph/AppInfo.java
index 2e352a2..047c498 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfo.java
@@ -3,7 +3,11 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import com.android.tools.r8.graph.ResolutionResult.ArrayCloneMethodResult;
+import com.android.tools.r8.graph.ResolutionResult.ClassNotFoundResult;
+import com.android.tools.r8.graph.ResolutionResult.IncompatibleClassResult;
 import com.android.tools.r8.graph.ResolutionResult.MultiResult;
+import com.android.tools.r8.graph.ResolutionResult.NoSuchMethodResult;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.google.common.collect.ImmutableMap;
@@ -265,7 +269,7 @@
     }
     DexClass definition = definitionFor(holder);
     if (definition == null) {
-      return ResolutionResult.EmptyResult.get();
+      return ClassNotFoundResult.INSTANCE;
     }
     return resolveMethod(definition, method);
   }
@@ -305,7 +309,7 @@
     assert checkIfObsolete();
     assert holder.isArrayType();
     if (method.name == dexItemFactory.cloneMethodName) {
-      return ResolutionResult.EmptyResult.get();
+      return ArrayCloneMethodResult.INSTANCE;
     } else {
       return resolveMethodOnClass(dexItemFactory.objectType, method);
     }
@@ -328,9 +332,12 @@
       return resolveMethodOnArray(holder, method);
     }
     DexClass clazz = definitionFor(holder);
+    if (clazz == null) {
+      return ClassNotFoundResult.INSTANCE;
+    }
     // Step 1: If holder is an interface, resolution fails with an ICCE. We return null.
-    if (clazz == null || clazz.isInterface()) {
-      return ResolutionResult.EmptyResult.get();
+    if (clazz.isInterface()) {
+      return IncompatibleClassResult.INSTANCE;
     }
     return resolveMethodOnClass(clazz, method);
   }
@@ -403,7 +410,7 @@
       return result;
     }
     // Return any of the non-default methods.
-    return anyTarget == null ? ResolutionResult.EmptyResult.get() : anyTarget;
+    return anyTarget == null ? NoSuchMethodResult.INSTANCE : anyTarget;
   }
 
   /**
@@ -459,14 +466,17 @@
   public ResolutionResult resolveMethodOnInterface(DexType holder, DexMethod desc) {
     assert checkIfObsolete();
     if (holder.isArrayType()) {
-      return ResolutionResult.EmptyResult.get();
+      return IncompatibleClassResult.INSTANCE;
     }
     // Step 1: Lookup interface.
     DexClass definition = definitionFor(holder);
     // If the definition is not an interface, resolution fails with an ICCE. We just return the
     // empty result here.
-    if (definition == null || !definition.isInterface()) {
-      return ResolutionResult.EmptyResult.get();
+    if (definition == null) {
+      return ClassNotFoundResult.INSTANCE;
+    }
+    if (!definition.isInterface()) {
+      return IncompatibleClassResult.INSTANCE;
     }
     return resolveMethodOnInterface(definition, desc);
   }
@@ -482,8 +492,7 @@
     // Step 3: Look for matching method on object class.
     DexClass objectClass = definitionFor(dexItemFactory.objectType);
     if (objectClass == null) {
-      // TODO(herhut): This should never happen. How do we handle missing classes?
-      return ResolutionResult.EmptyResult.get();
+      return ClassNotFoundResult.INSTANCE;
     }
     result = objectClass.lookupMethod(desc);
     if (result != null && result.accessFlags.isPublic() && !result.accessFlags.isAbstract()) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index d43583a..808fcc3 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -1274,6 +1274,13 @@
   }
 
   @Override
+  public boolean isValidVirtualTarget(InternalOptions options) {
+    return options.canUseNestBasedAccess()
+        ? (!accessFlags.isStatic() && !accessFlags.isConstructor())
+        : isVirtualMethod();
+  }
+
+  @Override
   public DexEncodedMethod asResultOfResolve() {
     checkIfObsolete();
     return this;
diff --git a/src/main/java/com/android/tools/r8/graph/ResolutionResult.java b/src/main/java/com/android/tools/r8/graph/ResolutionResult.java
index 4aeae4e..83a4132 100644
--- a/src/main/java/com/android/tools/r8/graph/ResolutionResult.java
+++ b/src/main/java/com/android/tools/r8/graph/ResolutionResult.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 import java.util.Collections;
@@ -12,6 +13,7 @@
 
 public interface ResolutionResult {
 
+  // TODO(b/140214802): Remove this method as its usage is questionable.
   DexEncodedMethod asResultOfResolve();
 
   DexEncodedMethod asSingleTarget();
@@ -22,8 +24,15 @@
 
   void forEachTarget(Consumer<DexEncodedMethod> consumer);
 
+  boolean isValidVirtualTarget(InternalOptions options);
+
+  default Set<DexEncodedMethod> lookupVirtualDispatchTargets(
+      boolean isInterface, AppInfoWithSubtyping appInfo) {
+    return isInterface ? lookupInterfaceTargets(appInfo) : lookupVirtualTargets(appInfo);
+  }
+
   default Set<DexEncodedMethod> lookupVirtualTargets(AppInfoWithSubtyping appInfo) {
-    // TODO(b/140016938): Don't allow this lookup on non-virtual resolutions.
+    assert isValidVirtualTarget(appInfo.app().options);
     // First add the target for receiver type method.type.
     Set<DexEncodedMethod> result = Sets.newIdentityHashSet();
     forEachTarget(result::add);
@@ -46,7 +55,7 @@
   }
 
   default Set<DexEncodedMethod> lookupInterfaceTargets(AppInfoWithSubtyping appInfo) {
-    // TODO(b/140016938): Don't allow this lookup on non-virtual resolutions.
+    assert isValidVirtualTarget(appInfo.app().options);
     Set<DexEncodedMethod> result = Sets.newIdentityHashSet();
     if (hasSingleTarget()) {
       // Add default interface methods to the list of targets.
@@ -122,6 +131,16 @@
     }
 
     @Override
+    public boolean isValidVirtualTarget(InternalOptions options) {
+      for (DexEncodedMethod method : methods) {
+        if (!method.isValidVirtualTarget(options)) {
+          return false;
+        }
+      }
+      return true;
+    }
+
+    @Override
     public DexEncodedMethod asResultOfResolve() {
       // Resolution may return any of the targets that were found.
       return methods.get(0);
@@ -149,17 +168,7 @@
     }
   }
 
-  class EmptyResult implements ResolutionResult {
-
-    private static final EmptyResult SINGLETON = new EmptyResult();
-
-    private EmptyResult() {
-      // Intentionally left empty.
-    }
-
-    static EmptyResult get() {
-      return SINGLETON;
-    }
+  abstract class EmptyResult implements ResolutionResult {
 
     @Override
     public DexEncodedMethod asResultOfResolve() {
@@ -196,4 +205,50 @@
       return null;
     }
   }
+
+  class ArrayCloneMethodResult extends EmptyResult {
+
+    static final ArrayCloneMethodResult INSTANCE = new ArrayCloneMethodResult();
+
+    private ArrayCloneMethodResult() {
+      // Intentionally left empty.
+    }
+
+    @Override
+    public boolean isValidVirtualTarget(InternalOptions options) {
+      return true;
+    }
+  }
+
+  abstract class FailedResolutionResult extends EmptyResult {
+
+    @Override
+    public boolean isValidVirtualTarget(InternalOptions options) {
+      return false;
+    }
+  }
+
+  class ClassNotFoundResult extends FailedResolutionResult {
+    static final ClassNotFoundResult INSTANCE = new ClassNotFoundResult();
+
+    private ClassNotFoundResult() {
+      // Intentionally left empty.
+    }
+  }
+
+  class IncompatibleClassResult extends FailedResolutionResult {
+    static final IncompatibleClassResult INSTANCE = new IncompatibleClassResult();
+
+    private IncompatibleClassResult() {
+      // Intentionally left empty.
+    }
+  }
+
+  class NoSuchMethodResult extends FailedResolutionResult {
+    static final NoSuchMethodResult INSTANCE = new NoSuchMethodResult();
+
+    private NoSuchMethodResult() {
+      // Intentionally left empty.
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilder.java
index 2322b7b..d1ca480 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilder.java
@@ -191,9 +191,10 @@
               method -> {
                 ResolutionResult resolution =
                     appView.appInfo().resolveMethod(method.holder, method, isInterface);
-                return isInterface
-                    ? resolution.lookupInterfaceTargets(appView.appInfo())
-                    : resolution.lookupVirtualTargets(appView.appInfo());
+                if (resolution.isValidVirtualTarget(appView.options())) {
+                  return resolution.lookupVirtualDispatchTargets(isInterface, appView.appInfo());
+                }
+                return null;
               });
       if (possibleTargets != null) {
         boolean likelySpuriousCallEdge =
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/InliningConstraints.java b/src/main/java/com/android/tools/r8/ir/optimize/InliningConstraints.java
index 32305eb..074160b 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/InliningConstraints.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/InliningConstraints.java
@@ -164,10 +164,6 @@
     DexMethod lookup = graphLense.lookupMethod(method);
     return forVirtualInvoke(
         lookup,
-        appView
-            .appInfo()
-            .resolveMethodOnInterface(lookup.holder, lookup)
-            .lookupInterfaceTargets(appView.appInfo()),
         invocationContext,
         true);
   }
@@ -199,10 +195,6 @@
     DexMethod lookup = graphLense.lookupMethod(method);
     return forVirtualInvoke(
         lookup,
-        appView
-            .appInfo()
-            .resolveMethodOnClass(lookup.holder, lookup)
-            .lookupVirtualTargets(appView.appInfo()),
         invocationContext,
         false);
   }
@@ -351,20 +343,20 @@
 
   private ConstraintWithTarget forVirtualInvoke(
       DexMethod method,
-      Collection<DexEncodedMethod> targets,
       DexType invocationContext,
       boolean isInterface) {
     if (method.holder.isArrayType()) {
       return ConstraintWithTarget.ALWAYS;
     }
-    if (targets == null) {
-      return ConstraintWithTarget.NEVER;
-    }
 
     // Perform resolution and derive inlining constraints based on the accessibility of the
     // resolution result.
     ResolutionResult resolutionResult =
         appView.appInfo().resolveMethod(method.holder, method, isInterface);
+    if (!resolutionResult.isValidVirtualTarget(appView.options())) {
+      return ConstraintWithTarget.NEVER;
+    }
+
     DexEncodedMethod resolutionTarget = resolutionResult.asResultOfResolve();
     if (resolutionTarget == null) {
       // This will fail at runtime.
@@ -393,6 +385,8 @@
 
     // For each of the actual potential targets, derive constraints based on the accessibility
     // of the method itself.
+    Collection<DexEncodedMethod> targets =
+        resolutionResult.lookupVirtualDispatchTargets(isInterface, appView.appInfo());
     for (DexEncodedMethod target : targets) {
       methodHolder = graphLense.lookupType(target.method.holder);
       assert appView.definitionFor(methodHolder) != null;
diff --git a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
index b0885a2..9df98de 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -867,7 +867,7 @@
     // First get the target for the holder type.
     ResolutionResult topMethod = resolveMethodOnClass(holder, method);
     // We might hit none or multiple targets. Both make this fail at runtime.
-    if (!topMethod.hasSingleTarget() || !topMethod.asSingleTarget().isVirtualMethod()) {
+    if (!topMethod.hasSingleTarget() || !topMethod.isValidVirtualTarget(app().options)) {
       method.setSingleVirtualMethodCache(refinedReceiverType, null);
       return null;
     }
@@ -1021,6 +1021,9 @@
     // First check that there is a target for this invoke-interface to hit. If there is none,
     // this will fail at runtime.
     ResolutionResult topTarget = resolveMethodOnInterface(holder, method);
+    if (!topTarget.isValidVirtualTarget(app().options)) {
+      return null;
+    }
     if (topTarget.asResultOfResolve() == null) {
       return null;
     }
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index 2bf674b..dc1a70e 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -1618,16 +1618,14 @@
 
     DexEncodedMethod resolutionTarget =
         findAndMarkResolutionTarget(method, interfaceInvoke, reason);
-    if (resolutionTarget == null) {
+    if (resolutionTarget == null || !resolutionTarget.isValidVirtualTarget(options)) {
       // There is no valid resolution, so any call will lead to a runtime exception.
       return;
     }
 
     assert interfaceInvoke == holder.isInterface();
     Set<DexEncodedMethod> possibleTargets =
-        holder.isInterface()
-            ? resolutionTarget.lookupInterfaceTargets(appInfo)
-            : resolutionTarget.lookupVirtualTargets(appInfo);
+        resolutionTarget.lookupVirtualDispatchTargets(interfaceInvoke, appInfo);
     if (possibleTargets == null || possibleTargets.isEmpty()) {
       return;
     }
@@ -1745,13 +1743,20 @@
     // Therefore, we need to continue resolution from the super type until we find a virtual method.
     if (resolutionTarget.isPrivateMethod() || resolutionTarget.isStatic()) {
       assert !interfaceInvoke || resolutionTargetClass.isInterface();
-      markPossiblyValidTarget(method, reason, resolutionTarget, resolutionTargetClass);
+      DexEncodedMethod possiblyValidTarget =
+          markPossiblyValidTarget(method, reason, resolutionTarget, resolutionTargetClass);
+      if (possiblyValidTarget != null) {
+        // Since some Art runtimes may actually end up targeting this method, it is returned as
+        // the basis of lookup for the enqueuing of virtual dispatches. Not doing so may cause it
+        // to be marked abstract, thus leading to an AbstractMethodError on said Art runtimes.
+        return possiblyValidTarget;
+      }
     }
 
     return resolutionTarget;
   }
 
-  private void markPossiblyValidTarget(
+  private DexEncodedMethod markPossiblyValidTarget(
       DexMethod method,
       KeepReason reason,
       DexEncodedMethod resolutionTarget,
@@ -1763,16 +1768,17 @@
                   resolutionTargetClass.superType, method, resolutionTargetClass.isInterface())
               .asResultOfResolve();
       if (resolutionTarget == null) {
-        return;
+        return null;
       }
       resolutionTargetClass = appInfo.definitionFor(resolutionTarget.method.holder);
       if (resolutionTargetClass == null) {
-        return;
+        return null;
       }
     }
     if (resolutionTargetClass.isProgramClass()) {
       markMethodAsTargeted(resolutionTarget, reason);
     }
+    return resolutionTarget;
   }
 
   private DexMethod generatedEnumValuesMethod(DexClass enumClass) {
diff --git a/src/test/java/com/android/tools/r8/resolution/SingleTargetLookupTest.java b/src/test/java/com/android/tools/r8/resolution/SingleTargetLookupTest.java
index 11265a9..f91f1bd 100644
--- a/src/test/java/com/android/tools/r8/resolution/SingleTargetLookupTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/SingleTargetLookupTest.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.resolution;
 
+import static org.junit.Assert.assertTrue;
+
 import com.android.tools.r8.AsmTestBase;
 import com.android.tools.r8.dex.ApplicationReader;
 import com.android.tools.r8.graph.AppInfo;
@@ -14,6 +16,7 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ResolutionResult;
 import com.android.tools.r8.resolution.singletarget.Main;
 import com.android.tools.r8.resolution.singletarget.one.AbstractSubClass;
 import com.android.tools.r8.resolution.singletarget.one.AbstractTopClass;
@@ -165,46 +168,87 @@
 
   @Parameters(name = "{1}.{0} -> {2}")
   public static List<Object[]> getData() {
-    return ImmutableList.copyOf(new Object[][]{
-        singleTarget("singleTargetAtTop", AbstractTopClass.class),
-        singleTargetWithAbstracts("singleShadowingOverride", AbstractTopClass.class,
-            AbstractSubClass.class, AbstractTopClass.class),
-        manyTargets("abstractTargetAtTop", AbstractTopClass.class, AbstractTopClass.class,
-            SubSubClassOne.class, SubSubClassTwo.class),
-        singleTargetWithAbstracts("overridenInAbstractClassOnly", AbstractTopClass.class,
-            AbstractTopClass.class, SubSubClassThree.class),
-        onlyUnreachableTargets("overridenInAbstractClassOnly", SubSubClassThree.class,
-            SubSubClassThree.class),
-        manyTargets("overriddenInTwoSubTypes", AbstractTopClass.class, AbstractTopClass.class,
-            SubSubClassOne.class, SubSubClassTwo.class),
-        manyTargets("definedInTwoSubTypes", AbstractTopClass.class, AbstractTopClass.class,
-            SubSubClassOne.class, SubSubClassTwo.class),
-        onlyUnreachableTargets("staticMethod", AbstractTopClass.class, AbstractTopClass.class),
-        manyTargets("overriddenInTwoSubTypes", OtherAbstractTopClass.class,
-            OtherAbstractTopClass.class, OtherSubSubClassOne.class, OtherSubSubClassTwo.class),
-        manyTargets("abstractOverriddenInTwoSubTypes", OtherAbstractTopClass.class,
-            OtherAbstractTopClass.class, OtherSubSubClassOne.class, OtherSubSubClassTwo.class),
-        manyTargets("overridesOnDifferentLevels", OtherAbstractTopClass.class,
-            OtherAbstractTopClass.class, OtherSubSubClassOne.class, OtherAbstractSubClassTwo.class),
-        singleTarget("defaultMethod", AbstractTopClass.class, InterfaceWithDefault.class),
-        manyTargets("overriddenDefault", AbstractTopClass.class, InterfaceWithDefault.class,
-            SubSubClassTwo.class),
-        singleTarget("overriddenDefault", SubSubClassTwo.class),
-        singleTarget("overriddenByIrrelevantInterface", AbstractTopClass.class),
-        singleTarget("overriddenByIrrelevantInterface", SubSubClassOne.class,
-            AbstractTopClass.class),
-        manyTargets("overriddenInOtherInterface", AbstractTopClass.class,
-            InterfaceWithDefault.class,
-            IrrelevantInterfaceWithDefault.class),
-        manyTargets("overriddenInOtherInterface", SubSubClassOne.class,
-            InterfaceWithDefault.class,
-            IrrelevantInterfaceWithDefault.class),
-        manyTargets("abstractMethod", ThirdAbstractTopClass.class, ThirdAbstractTopClass.class,
-            ThirdSubClassOne.class),
-        singleTarget("instanceMethod", ThirdAbstractTopClass.class, ThirdAbstractTopClass.class),
-        singleTarget(
-            "otherInstanceMethod", ThirdAbstractTopClass.class, ThirdAbstractTopClass.class),
-    });
+    return ImmutableList.copyOf(
+        new Object[][] {
+          singleTarget("singleTargetAtTop", AbstractTopClass.class),
+          singleTargetWithAbstracts(
+              "singleShadowingOverride",
+              AbstractTopClass.class,
+              AbstractSubClass.class,
+              AbstractTopClass.class),
+          manyTargets(
+              "abstractTargetAtTop",
+              AbstractTopClass.class,
+              AbstractTopClass.class,
+              SubSubClassOne.class,
+              SubSubClassTwo.class),
+          singleTargetWithAbstracts(
+              "overridenInAbstractClassOnly",
+              AbstractTopClass.class,
+              AbstractTopClass.class,
+              SubSubClassThree.class),
+          onlyUnreachableTargets(
+              "overridenInAbstractClassOnly", SubSubClassThree.class, SubSubClassThree.class),
+          manyTargets(
+              "overriddenInTwoSubTypes",
+              AbstractTopClass.class,
+              AbstractTopClass.class,
+              SubSubClassOne.class,
+              SubSubClassTwo.class),
+          manyTargets(
+              "definedInTwoSubTypes",
+              AbstractTopClass.class,
+              AbstractTopClass.class,
+              SubSubClassOne.class,
+              SubSubClassTwo.class),
+          onlyUnreachableTargets("staticMethod", AbstractTopClass.class),
+          manyTargets(
+              "overriddenInTwoSubTypes",
+              OtherAbstractTopClass.class,
+              OtherAbstractTopClass.class,
+              OtherSubSubClassOne.class,
+              OtherSubSubClassTwo.class),
+          manyTargets(
+              "abstractOverriddenInTwoSubTypes",
+              OtherAbstractTopClass.class,
+              OtherAbstractTopClass.class,
+              OtherSubSubClassOne.class,
+              OtherSubSubClassTwo.class),
+          manyTargets(
+              "overridesOnDifferentLevels",
+              OtherAbstractTopClass.class,
+              OtherAbstractTopClass.class,
+              OtherSubSubClassOne.class,
+              OtherAbstractSubClassTwo.class),
+          singleTarget("defaultMethod", AbstractTopClass.class, InterfaceWithDefault.class),
+          manyTargets(
+              "overriddenDefault",
+              AbstractTopClass.class,
+              InterfaceWithDefault.class,
+              SubSubClassTwo.class),
+          singleTarget("overriddenDefault", SubSubClassTwo.class),
+          singleTarget("overriddenByIrrelevantInterface", AbstractTopClass.class),
+          singleTarget(
+              "overriddenByIrrelevantInterface", SubSubClassOne.class, AbstractTopClass.class),
+          manyTargets(
+              "overriddenInOtherInterface",
+              AbstractTopClass.class,
+              InterfaceWithDefault.class,
+              IrrelevantInterfaceWithDefault.class),
+          manyTargets(
+              "overriddenInOtherInterface",
+              SubSubClassOne.class,
+              InterfaceWithDefault.class,
+              IrrelevantInterfaceWithDefault.class),
+          manyTargets(
+              "abstractMethod",
+              ThirdAbstractTopClass.class,
+              ThirdAbstractTopClass.class,
+              ThirdSubClassOne.class),
+          singleTarget("instanceMethod", ThirdAbstractTopClass.class, ThirdAbstractTopClass.class),
+          singleTarget(
+              "otherInstanceMethod", ThirdAbstractTopClass.class, ThirdAbstractTopClass.class),
+        });
   }
 
   public static DexMethod buildMethod(Class clazz, String name, AppInfo appInfo) {
@@ -247,12 +291,16 @@
     DexMethod method = buildMethod(invokeReceiver, methodName, appInfo);
     Assert.assertNotNull(
         appInfo.resolveMethod(toType(invokeReceiver, appInfo), method).asResultOfResolve());
-    Set<DexEncodedMethod> targets =
-        appInfo.resolveMethod(method.holder, method).lookupVirtualTargets(appInfo);
-    Set<DexType> targetHolders = targets.stream().map(m -> m.method.holder)
-        .collect(Collectors.toSet());
-    Assert.assertEquals(allTargetHolders.size(), targetHolders.size());
-    Assert.assertTrue(
-        allTargetHolders.stream().map(t -> toType(t, appInfo)).allMatch(targetHolders::contains));
+    ResolutionResult resolutionResult = appInfo.resolveMethod(method.holder, method);
+    if (resolutionResult.isValidVirtualTarget(appInfo.app().options)) {
+      Set<DexEncodedMethod> targets = resolutionResult.lookupVirtualTargets(appInfo);
+      Set<DexType> targetHolders =
+          targets.stream().map(m -> m.method.holder).collect(Collectors.toSet());
+      Assert.assertEquals(allTargetHolders.size(), targetHolders.size());
+      assertTrue(
+          allTargetHolders.stream().map(t -> toType(t, appInfo)).allMatch(targetHolders::contains));
+    } else {
+      assertTrue(allTargetHolders.isEmpty());
+    }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfPrivateStaticMethodTest.java b/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfPrivateStaticMethodTest.java
index d0806db..e914a7b 100644
--- a/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfPrivateStaticMethodTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfPrivateStaticMethodTest.java
@@ -5,7 +5,7 @@
 
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.TestBase;
@@ -19,7 +19,6 @@
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.util.List;
-import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import org.junit.Assert;
 import org.junit.BeforeClass;
@@ -83,8 +82,9 @@
 
   @Test
   public void lookupSingleTarget() {
-    DexEncodedMethod resolved =
-        appInfo.resolveMethodOnClass(methodOnB.holder, methodOnB).asResultOfResolve();
+    ResolutionResult resolutionResult = appInfo.resolveMethodOnClass(methodOnB.holder, methodOnB);
+    assertFalse(resolutionResult.isValidVirtualTarget(appInfo.app().options));
+    DexEncodedMethod resolved = resolutionResult.asSingleTarget();
     assertEquals(methodOnA, resolved.method);
     DexEncodedMethod singleVirtualTarget =
         appInfo.lookupSingleVirtualTarget(methodOnB, methodOnB.holder);
@@ -94,14 +94,9 @@
   @Test
   public void lookupVirtualTargets() {
     ResolutionResult resolutionResult = appInfo.resolveMethodOnClass(methodOnB.holder, methodOnB);
-    DexEncodedMethod resolved = resolutionResult.asResultOfResolve();
+    DexEncodedMethod resolved = resolutionResult.asSingleTarget();
     assertEquals(methodOnA, resolved.method);
-    // This behavior is up for debate as the resolution target is A.f which is private static, thus
-    // the runtime behavior will be to throw a runtime exception. Thus it would be reasonable to
-    // return the empty set as no targets can actually be hit at runtime.
-    Set<DexEncodedMethod> targets = resolutionResult.lookupVirtualTargets(appInfo);
-    assertTrue("Expected " + methodOnA, targets.stream().anyMatch(m -> m.method == methodOnA));
-    assertTrue("Expected " + methodOnC, targets.stream().anyMatch(m -> m.method == methodOnC));
+    assertFalse(resolutionResult.isValidVirtualTarget(appInfo.app().options));
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfPrivateStaticMethodWithVirtualParentTest.java b/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfPrivateStaticMethodWithVirtualParentTest.java
index 6c61cc5..8d867d8 100644
--- a/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfPrivateStaticMethodWithVirtualParentTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfPrivateStaticMethodWithVirtualParentTest.java
@@ -5,7 +5,7 @@
 
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
 
 import com.android.tools.r8.AsmTestBase;
 import com.android.tools.r8.R8TestRunResult;
@@ -20,7 +20,6 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.google.common.collect.ImmutableList;
 import java.util.List;
-import java.util.Set;
 import org.junit.Assert;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -144,12 +143,9 @@
   @Test
   public void lookupVirtualTargets() {
     ResolutionResult resolutionResult = appInfo.resolveMethodOnClass(methodOnB.holder, methodOnB);
-    DexEncodedMethod resolved = resolutionResult.asResultOfResolve();
+    DexEncodedMethod resolved = resolutionResult.asSingleTarget();
     assertEquals(methodOnA, resolved.method);
-    // See comment in VirtualOverrideOfPrivateStaticMethodTest.lookupVirtualTargets().
-    Set<DexEncodedMethod> targets = resolutionResult.lookupVirtualTargets(appInfo);
-    assertTrue("Expected " + methodOnA, targets.stream().anyMatch(m -> m.method == methodOnA));
-    assertTrue("Expected " + methodOnC, targets.stream().anyMatch(m -> m.method == methodOnC));
+    assertFalse(resolutionResult.isValidVirtualTarget(appInfo.app().options));
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfStaticMethodWithVirtualParentInterfaceTest.java b/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfStaticMethodWithVirtualParentInterfaceTest.java
index 0be4846..f968c03 100644
--- a/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfStaticMethodWithVirtualParentInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfStaticMethodWithVirtualParentInterfaceTest.java
@@ -5,7 +5,7 @@
 
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
 
 import com.android.tools.r8.AsmTestBase;
 import com.android.tools.r8.R8TestRunResult;
@@ -19,7 +19,6 @@
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.google.common.collect.ImmutableList;
 import java.util.List;
-import java.util.Set;
 import org.junit.Assert;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -120,24 +119,22 @@
 
   @Test
   public void lookupSingleTarget() {
-    DexEncodedMethod resolved =
-        appInfo.resolveMethodOnInterface(methodOnB.holder, methodOnB).asResultOfResolve();
+    ResolutionResult resolutionResult =
+        appInfo.resolveMethodOnInterface(methodOnB.holder, methodOnB);
+    DexEncodedMethod resolved = resolutionResult.asSingleTarget();
     assertEquals(methodOnB, resolved.method);
+    assertFalse(resolutionResult.isValidVirtualTarget(appInfo.app().options));
     DexEncodedMethod singleVirtualTarget =
         appInfo.lookupSingleInterfaceTarget(methodOnB, methodOnB.holder);
-    // TODO(b/140088797): This should not conclude a single target.
-    Assert.assertNotNull(singleVirtualTarget);
+    Assert.assertNull(singleVirtualTarget);
   }
 
   @Test
   public void lookupVirtualTargets() {
     ResolutionResult resolutionResult = appInfo.resolveMethodOnInterface(methodOnB.holder, methodOnB);
-    DexEncodedMethod resolved = resolutionResult.asResultOfResolve();
+    DexEncodedMethod resolved = resolutionResult.asSingleTarget();
     assertEquals(methodOnB, resolved.method);
-    Set<DexEncodedMethod> targets = resolutionResult.lookupInterfaceTargets(appInfo);
-    // TODO(b/140088797): This should not conclude a single target.
-    assertTrue("Expected " + methodOnC, targets.stream().anyMatch(m -> m.method == methodOnC));
-    assertEquals(1, targets.size());
+    assertFalse(resolutionResult.isValidVirtualTarget(appInfo.app().options));
   }
 
   @Test
@@ -162,17 +159,30 @@
 
   @Test
   public void runR8() throws Exception {
+    runR8(true);
+  }
+
+  @Test
+  public void runR8NoMerging() throws Exception {
+    runR8(false);
+  }
+
+  public void runR8(boolean enableVerticalClassMerging) throws Exception {
     R8TestRunResult runResult =
         testForR8(parameters.getBackend())
             .addProgramClasses(CLASSES)
             .addProgramClassFileData(DUMPS)
             .addKeepMainRule(Main.class)
             .setMinApi(parameters.getApiLevel())
-            // TODO(b/140088797): Once fixed, parameterize on the merger as it appears to fail.
-            .addOptionsModification(o -> o.enableVerticalClassMerging = false)
+            .addOptionsModification(o -> o.enableVerticalClassMerging = enableVerticalClassMerging)
             .run(parameters.getRuntime(), Main.class);
-    // TODO(b/140088797): Due to the single target lookup R8 incorrectly inlines the call C.f().
-    runResult.assertSuccessWithOutputLines("Called C.f");
+    if (enableVerticalClassMerging) {
+      // Vertical class merging will merge B and C and change the instruction to invoke-virtual
+      // causing the legacy ART runtime behavior to match the expected error.
+      runResult.assertFailureWithErrorThatMatches(containsString("IncompatibleClassChangeError"));
+    } else {
+      checkResult(runResult);
+    }
   }
 
   private void checkResult(TestRunResult<?> runResult) {
diff --git a/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfStaticMethodWithVirtualParentTest.java b/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfStaticMethodWithVirtualParentTest.java
index 7bec778..20c3cab 100644
--- a/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfStaticMethodWithVirtualParentTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/VirtualOverrideOfStaticMethodWithVirtualParentTest.java
@@ -5,7 +5,7 @@
 
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
 
 import com.android.tools.r8.AsmTestBase;
 import com.android.tools.r8.R8TestRunResult;
@@ -20,7 +20,6 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.google.common.collect.ImmutableList;
 import java.util.List;
-import java.util.Set;
 import org.junit.Assert;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -167,9 +166,10 @@
 
   @Test
   public void lookupSingleTarget() {
-    DexEncodedMethod resolved =
-        appInfo.resolveMethodOnClass(methodOnB.holder, methodOnB).asResultOfResolve();
+    ResolutionResult resolutionResult = appInfo.resolveMethodOnClass(methodOnB.holder, methodOnB);
+    DexEncodedMethod resolved = resolutionResult.asSingleTarget();
     assertEquals(methodOnA, resolved.method);
+    assertFalse(resolutionResult.isValidVirtualTarget(appInfo.app().options));
     DexEncodedMethod singleVirtualTarget =
         appInfo.lookupSingleVirtualTarget(methodOnB, methodOnB.holder);
     Assert.assertNull(singleVirtualTarget);
@@ -180,10 +180,7 @@
     ResolutionResult resolutionResult = appInfo.resolveMethodOnClass(methodOnB.holder, methodOnB);
     DexEncodedMethod resolved = resolutionResult.asResultOfResolve();
     assertEquals(methodOnA, resolved.method);
-    // See comment in VirtualOverrideOfPrivateStaticMethodTest.lookupVirtualTargets().
-    Set<DexEncodedMethod> targets = resolutionResult.lookupVirtualTargets(appInfo);
-    assertTrue("Expected " + methodOnA, targets.stream().anyMatch(m -> m.method == methodOnA));
-    assertTrue("Expected " + methodOnC, targets.stream().anyMatch(m -> m.method == methodOnC));
+    assertFalse(resolutionResult.isValidVirtualTarget(appInfo.app().options));
   }
 
   @Test