Compute if LookupResult is incomplete based on keep and library

When querying for dispatch targets, we may not know all the methods
implemented based on library and keep rules. This CL adds multiple
tests for when to mark the result as complete and incomplete and
alters the lookup algorithm to satisfy the tests.

This is not complete yet since we do not still account for call sites

Bug: 149941117
Change-Id: I28ca057baf3badad31695bfdcf6848132adb2489
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java b/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
index 9a07dd2..105868f 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
@@ -43,7 +43,6 @@
       Consumer<DexCallSite> callSiteConsumer) {
     WorkList<DexType> workList = WorkList.newIdentityWorkList();
     workList.addIfNotSeen(type);
-    workList.addIfNotSeen(allImmediateSubtypes(type));
     while (workList.hasNext()) {
       DexType subType = workList.next();
       DexProgramClass clazz = definitionForProgramType(subType);
diff --git a/src/main/java/com/android/tools/r8/graph/LookupCompletenessHelper.java b/src/main/java/com/android/tools/r8/graph/LookupCompletenessHelper.java
new file mode 100644
index 0000000..bfc0eb4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/LookupCompletenessHelper.java
@@ -0,0 +1,67 @@
+// Copyright (c) 2020, 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.graph;
+
+import com.android.tools.r8.graph.LookupResult.LookupResultSuccess.LookupResultCollectionState;
+import com.android.tools.r8.utils.WorkList;
+import com.google.common.collect.Sets;
+import java.util.Set;
+
+public class LookupCompletenessHelper {
+
+  private final PinnedPredicate pinnedPredicate;
+
+  private Set<DexType> pinnedInstantiations;
+
+  LookupCompletenessHelper(PinnedPredicate pinnedPredicate) {
+    this.pinnedPredicate = pinnedPredicate;
+  }
+
+  void checkClass(DexClass clazz) {
+    if (pinnedPredicate.isPinned(clazz.type)) {
+      if (pinnedInstantiations == null) {
+        pinnedInstantiations = Sets.newIdentityHashSet();
+      }
+      pinnedInstantiations.add(clazz.type);
+    }
+  }
+
+  LookupResultCollectionState computeCollectionState(
+      DexMethod method, AppView<? extends AppInfoWithClassHierarchy> appView) {
+    assert pinnedInstantiations == null || !pinnedInstantiations.isEmpty();
+    if (pinnedInstantiations == null) {
+      return LookupResultCollectionState.Complete;
+    }
+    WorkList<DexType> workList = WorkList.newIdentityWorkList(pinnedInstantiations);
+    while (workList.hasNext()) {
+      if (isMethodKeptInSuperTypeOrIsLibrary(workList, method, appView)) {
+        return LookupResultCollectionState.Incomplete;
+      }
+    }
+    return LookupResultCollectionState.Complete;
+  }
+
+  private boolean isMethodKeptInSuperTypeOrIsLibrary(
+      WorkList<DexType> workList,
+      DexMethod method,
+      AppView<? extends AppInfoWithClassHierarchy> appView) {
+    while (workList.hasNext()) {
+      DexClass parent = appView.definitionFor(workList.next());
+      if (parent == null) {
+        continue;
+      }
+      DexEncodedMethod methodInClass = parent.lookupVirtualMethod(method);
+      if (methodInClass != null
+          && (parent.isNotProgramClass() || pinnedPredicate.isPinned(methodInClass.method))) {
+        return true;
+      }
+      if (parent.superType != null) {
+        workList.addIfNotSeen(parent.superType);
+      }
+      workList.addIfNotSeen(parent.interfaces.values);
+    }
+    return false;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/LookupResult.java b/src/main/java/com/android/tools/r8/graph/LookupResult.java
index 7a923de..fd71a8d 100644
--- a/src/main/java/com/android/tools/r8/graph/LookupResult.java
+++ b/src/main/java/com/android/tools/r8/graph/LookupResult.java
@@ -75,6 +75,10 @@
       return state == LookupResultCollectionState.Incomplete;
     }
 
+    public boolean isComplete() {
+      return state == LookupResultCollectionState.Complete;
+    }
+
     public enum LookupResultCollectionState {
       Complete,
       Incomplete,
diff --git a/src/main/java/com/android/tools/r8/graph/PinnedPredicate.java b/src/main/java/com/android/tools/r8/graph/PinnedPredicate.java
new file mode 100644
index 0000000..d81becc
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/PinnedPredicate.java
@@ -0,0 +1,11 @@
+// Copyright (c) 2020, 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.graph;
+
+@FunctionalInterface
+public interface PinnedPredicate {
+
+  boolean isPinned(DexReference reference);
+}
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 4d2306c..bb10b76 100644
--- a/src/main/java/com/android/tools/r8/graph/ResolutionResult.java
+++ b/src/main/java/com/android/tools/r8/graph/ResolutionResult.java
@@ -81,11 +81,14 @@
   public abstract LookupResult lookupVirtualDispatchTargets(
       DexProgramClass context,
       AppView<? extends AppInfoWithClassHierarchy> appView,
-      InstantiatedSubTypeInfo instantiatedInfo);
+      InstantiatedSubTypeInfo instantiatedInfo,
+      PinnedPredicate pinnedPredicate);
 
   public final LookupResult lookupVirtualDispatchTargets(
       DexProgramClass context, AppView<AppInfoWithLiveness> appView) {
-    return lookupVirtualDispatchTargets(context, appView, appView.appInfo());
+    AppInfoWithLiveness appInfoWithLiveness = appView.appInfo();
+    return lookupVirtualDispatchTargets(
+        context, appView, appInfoWithLiveness, appInfoWithLiveness::isPinned);
   }
 
   public abstract DexClassAndMethod lookupVirtualDispatchTarget(
@@ -331,7 +334,8 @@
     public LookupResult lookupVirtualDispatchTargets(
         DexProgramClass context,
         AppView<? extends AppInfoWithClassHierarchy> appView,
-        InstantiatedSubTypeInfo instantiatedInfo) {
+        InstantiatedSubTypeInfo instantiatedInfo,
+        PinnedPredicate pinnedPredicate) {
       // Check that the initial resolution holder is accessible from the context.
       assert appView.isSubtype(initialResolutionHolder.type, resolvedHolder.type).isTrue()
           : initialResolutionHolder.type + " is not a subtype of " + resolvedHolder.type;
@@ -342,14 +346,23 @@
         // If the resolved reference is private there is no dispatch.
         // This is assuming that the method is accessible, which implies self/nest access.
         // Only include if the target has code or is native.
+        boolean isIncomplete =
+            pinnedPredicate.isPinned(resolvedHolder.type)
+                && pinnedPredicate.isPinned(resolvedMethod.method);
         return LookupResult.createResult(
-            Collections.singleton(resolvedMethod), LookupResultCollectionState.Complete);
+            Collections.singleton(resolvedMethod),
+            isIncomplete
+                ? LookupResultCollectionState.Incomplete
+                : LookupResultCollectionState.Complete);
       }
       assert resolvedMethod.isNonPrivateVirtualMethod();
       Set<DexEncodedMethod> result = Sets.newIdentityHashSet();
+      LookupCompletenessHelper incompleteness = new LookupCompletenessHelper(pinnedPredicate);
+      // TODO(b/150171154): Use instantiationHolder below.
       instantiatedInfo.forEachInstantiatedSubType(
           resolvedHolder.type,
           subClass -> {
+            incompleteness.checkClass(subClass);
             DexClassAndMethod dexClassAndMethod =
                 lookupVirtualDispatchTarget(subClass, appView, resolvedHolder.type);
             if (dexClassAndMethod != null) {
@@ -361,7 +374,8 @@
             // TODO(b/148769279): We need to look at the call site to see if it overrides
             //   the resolved method or not.
           });
-      return LookupResult.createResult(result, LookupResultCollectionState.Complete);
+      return LookupResult.createResult(
+          result, incompleteness.computeCollectionState(resolvedMethod.method, appView));
     }
 
     private static void addVirtualDispatchTarget(
@@ -584,7 +598,8 @@
     public LookupResult lookupVirtualDispatchTargets(
         DexProgramClass context,
         AppView<? extends AppInfoWithClassHierarchy> appView,
-        InstantiatedSubTypeInfo instantiatedInfo) {
+        InstantiatedSubTypeInfo instantiatedInfo,
+        PinnedPredicate pinnedPredicate) {
       return LookupResult.getIncompleteEmptyResult();
     }
 
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 1b481d1..4d8b108 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -2178,7 +2178,7 @@
     LookupResult lookupResult =
         // TODO(b/140214802): Call on the resolution once proper resolution and lookup is resolved.
         new SingleResolutionResult(holder, resolution.holder, resolution.method)
-            .lookupVirtualDispatchTargets(context, appView, appInfo);
+            .lookupVirtualDispatchTargets(context, appView, appInfo, pinnedItems::contains);
     if (!lookupResult.isLookupResultSuccess()) {
       return;
     }
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 835c528..d389826 100644
--- a/src/main/java/com/android/tools/r8/utils/WorkList.java
+++ b/src/main/java/com/android/tools/r8/utils/WorkList.java
@@ -19,8 +19,10 @@
     return new WorkList<T>(EqualityTest.IDENTITY);
   }
 
-  public WorkList() {
-    this(EqualityTest.HASH);
+  public static <T> WorkList<T> newIdentityWorkList(Iterable<T> items) {
+    WorkList<T> workList = new WorkList<>(EqualityTest.IDENTITY);
+    workList.addIfNotSeen(items);
+    return workList;
   }
 
   private WorkList(EqualityTest equalityTest) {
@@ -35,6 +37,12 @@
     items.forEach(this::addIfNotSeen);
   }
 
+  public void addIfNotSeen(T[] items) {
+    for (T item : items) {
+      addIfNotSeen(item);
+    }
+  }
+
   public void addIfNotSeen(T item) {
     if (seen.add(item)) {
       workingList.addLast(item);
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 5cc8194..12bc06c 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -600,6 +600,15 @@
 
   protected static AppView<AppInfoWithLiveness> computeAppViewWithLiveness(
       AndroidApp app, Class<?> mainClass) throws Exception {
+    return computeAppViewWithLiveness(
+        app, factory -> buildKeepRuleForClassAndMethods(mainClass, factory));
+  }
+
+  protected static AppView<AppInfoWithLiveness> computeAppViewWithLiveness(
+      AndroidApp app,
+      Function<DexItemFactory, Collection<ProguardConfigurationRule>>
+          proguardConfigurationRulesGenerator)
+      throws Exception {
     AppView<AppInfoWithSubtyping> appView = computeAppViewWithSubtyping(app);
     // Run the tree shaker to compute an instance of AppInfoWithLiveness.
     ExecutorService executor = Executors.newSingleThreadExecutor();
@@ -608,7 +617,7 @@
         new RootSetBuilder(
                 appView,
                 application,
-                buildKeepRuleForClassAndMethods(mainClass, application.dexItemFactory))
+                proguardConfigurationRulesGenerator.apply(appView.appInfo().dexItemFactory()))
             .run(executor);
     AppInfoWithLiveness appInfoWithLiveness =
         EnqueuerFactory.createForInitialTreeShaking(appView)
@@ -662,7 +671,7 @@
         ListUtils.map(formalTypes, type -> buildType(type, factory)));
   }
 
-  private static List<ProguardConfigurationRule> buildKeepRuleForClass(
+  protected static List<ProguardConfigurationRule> buildKeepRuleForClass(
       Class<?> clazz, DexItemFactory factory) {
     Builder keepRuleBuilder = ProguardKeepRule.builder();
     keepRuleBuilder.setSource("buildKeepRuleForClass " + clazz.getTypeName());
@@ -674,10 +683,10 @@
     return Collections.singletonList(keepRuleBuilder.build());
   }
 
-  private static List<ProguardConfigurationRule> buildKeepRuleForClassAndMethods(
+  protected static List<ProguardConfigurationRule> buildKeepRuleForClassAndMethods(
       Class<?> clazz, DexItemFactory factory) {
     Builder keepRuleBuilder = ProguardKeepRule.builder();
-    keepRuleBuilder.setSource("buildKeepRuleForClass " + clazz.getTypeName());
+    keepRuleBuilder.setSource("buildKeepRuleForClassAndMethods " + clazz.getTypeName());
     keepRuleBuilder.setType(ProguardKeepRuleType.KEEP);
     keepRuleBuilder.setClassNames(
         ProguardClassNameList.singletonList(
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreLookupTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreLookupTest.java
index 774209e..88251be 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreLookupTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreLookupTest.java
@@ -69,7 +69,8 @@
     // Check lookup targets with include method.
     ResolutionResult resolutionResult = appInfo().resolveMethodOnClass(clazz, method.method);
     LookupResult lookupResult =
-        resolutionResult.lookupVirtualDispatchTargets(clazz, appView, appInfo());
+        resolutionResult.lookupVirtualDispatchTargets(
+            clazz, appView, appInfo(), dexReference -> false);
     assertTrue(lookupResult.isLookupResultSuccess());
     Set<DexEncodedMethod> targets = lookupResult.asLookupResultSuccess().getMethodTargets();
     assertTrue(targets.contains(method));
@@ -79,7 +80,7 @@
     LookupResult lookupResult =
         appInfo()
             .resolveMethodOnInterface(clazz, method.method)
-            .lookupVirtualDispatchTargets(clazz, appView, appInfo());
+            .lookupVirtualDispatchTargets(clazz, appView, appInfo(), dexReference -> false);
     assertTrue(lookupResult.isLookupResultSuccess());
     Set<DexEncodedMethod> targets = lookupResult.asLookupResultSuccess().getMethodTargets();
     if (appInfo().subtypes(method.method.holder).stream()
diff --git a/src/test/java/com/android/tools/r8/resolution/virtualtargets/KeptTargetsIncompleteLookupTest.java b/src/test/java/com/android/tools/r8/resolution/virtualtargets/KeptTargetsIncompleteLookupTest.java
new file mode 100644
index 0000000..e74a3a7
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/resolution/virtualtargets/KeptTargetsIncompleteLookupTest.java
@@ -0,0 +1,380 @@
+// Copyright (c) 2020, 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.resolution.virtualtargets;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedMethod;
+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.LookupResult;
+import com.android.tools.r8.graph.LookupResult.LookupResultSuccess;
+import com.android.tools.r8.graph.ResolutionResult;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.ProguardConfigurationRule;
+import com.google.common.collect.ImmutableSet;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class KeptTargetsIncompleteLookupTest extends TestBase {
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public KeptTargetsIncompleteLookupTest(TestParameters parameters) {
+    // Empty to satisfy construction of none-runtime.
+  }
+
+  private LookupResultSuccess testLookup(Class<?> methodToBeKept) throws Exception {
+    return testLookup(B.class, methodToBeKept, methodToBeKept, A.class, C.class);
+  }
+
+  private LookupResultSuccess testLookup(
+      Class<?> initial,
+      Class<?> methodToBeKept,
+      Class<?> classToBeKept,
+      Class<?>... expectedMethodHolders)
+      throws Exception {
+    return testLookup(
+        initial,
+        methodToBeKept,
+        classToBeKept,
+        Arrays.asList(expectedMethodHolders),
+        Arrays.asList(I.class, A.class, B.class, C.class));
+  }
+
+  private LookupResultSuccess testLookup(
+      Class<?> initial,
+      Class<?> methodToBeKept,
+      Class<?> classToBeKept,
+      Collection<Class<?>> expectedMethodHolders,
+      Collection<Class<?>> classes)
+      throws Exception {
+    AppView<AppInfoWithLiveness> appView =
+        computeAppViewWithLiveness(
+            buildClasses(classes).build(),
+            factory -> {
+              List<ProguardConfigurationRule> rules = new ArrayList<>();
+              rules.addAll(buildKeepRuleForClassAndMethods(methodToBeKept, factory));
+              rules.addAll(buildKeepRuleForClass(classToBeKept, factory));
+              return rules;
+            });
+    AppInfoWithLiveness appInfo = appView.appInfo();
+    DexMethod method = buildNullaryVoidMethod(initial, "foo", appInfo.dexItemFactory());
+    ResolutionResult resolutionResult = appInfo.resolveMethod(method.holder, method);
+    DexProgramClass context =
+        appView.definitionForProgramType(buildType(Unrelated.class, appInfo.dexItemFactory()));
+    LookupResult lookupResult = resolutionResult.lookupVirtualDispatchTargets(context, appView);
+    assertTrue(lookupResult.isLookupResultSuccess());
+    LookupResultSuccess lookupResultSuccess = lookupResult.asLookupResultSuccess();
+    Set<String> targets =
+        lookupResultSuccess.getMethodTargets().stream()
+            .map(DexEncodedMethod::qualifiedName)
+            .collect(Collectors.toSet());
+    Set<String> expected =
+        expectedMethodHolders.stream()
+            .map(c -> c.getTypeName() + ".foo")
+            .collect(Collectors.toSet());
+    assertEquals(expected, targets);
+    return lookupResultSuccess;
+  }
+
+  @Test
+  public void testCompleteLookupResultWhenKeepingUnrelated() throws Exception {
+    // I {
+    //   foo()
+    // }
+    //
+    // A implements I {
+    //   foo() <-- resolved
+    // }
+    //
+    // B extends A { } <-- initial
+    //
+    // C extends B {
+    //   foo()
+    // }
+    assertTrue(
+        testLookup(B.class, Unrelated.class, Unrelated.class, A.class, C.class).isComplete());
+  }
+
+  @Test
+  public void testKeptResolvedAndNoKeepInSubtreeFromInitial() throws Exception {
+    // I {
+    //   foo()
+    // }
+    //
+    // A implements I {
+    //   foo() <-- kept, resolved
+    // }
+    //
+    // B extends A { } <-- initial
+    //
+    // C extends B {
+    //   foo()
+    // }
+    assertTrue(testLookup(A.class).isComplete());
+  }
+
+  @Test
+  public void testIncompleteLookupResultWhenKeepingStaticReceiver() throws Exception {
+    // I {
+    //   foo()
+    // }
+    //
+    // A implements I {
+    //   foo() <-- resolved
+    // }
+    //
+    // B extends A { } <-- initial, kept
+    //
+    // C extends B {
+    //   foo()
+    // }
+    assertTrue(testLookup(B.class).isIncomplete());
+  }
+
+  @Test
+  public void testIncompleteLookupResultWhenKeepingSubTypeMethod() throws Exception {
+    // I {
+    //   foo()
+    // }
+    //
+    // A implements I {
+    //   foo() <-- resolved
+    // }
+    //
+    // B extends A { } <-- initial
+    //
+    // C extends B {
+    //   foo() <-- kept
+    // }
+    assertTrue(testLookup(C.class).isIncomplete());
+  }
+
+  @Test
+  public void testIncompleteLookupResultWhenKeepingMethodOnParentToResolveAndKeepClass()
+      throws Exception {
+    // I {
+    //   foo()
+    // }
+    //
+    // A implements I {
+    //   foo() <-- kept
+    // }
+    //
+    // B extends A { }
+    //
+    // C extends B { <-- initial, resolved, kept
+    //   foo()
+    // }
+    assertTrue(testLookup(C.class, A.class, C.class, C.class).isIncomplete());
+  }
+
+  @Test
+  public void testCompleteLookupResultWhenKeepingMethodOnParentToResolveAndNotKeepClass()
+      throws Exception {
+    // I {
+    //   foo()
+    // }
+    //
+    // A implements I {
+    //   foo() <-- kept
+    // }
+    //
+    // B extends A { }
+    //
+    // C extends B { <-- initial, resolved
+    //   foo()
+    // }
+    assertTrue(testLookup(C.class, A.class, Unrelated.class, C.class).isComplete());
+  }
+
+  @Test
+  public void testLibraryWithNoOverride() throws Exception {
+    // ----- Library -----
+    // I {
+    //   foo()
+    // }
+    //
+    // A implements I {
+    //   foo() <-- resolved
+    // }
+    //
+    // ----- Program -----
+    // B extends A { } <-- initial
+    AppView<AppInfoWithSubtyping> appView =
+        computeAppViewWithSubtyping(
+            buildClasses(Collections.singletonList(B.class), Arrays.asList(A.class, I.class))
+                .build());
+    AppInfoWithSubtyping appInfo = appView.appInfo();
+    DexMethod method = buildNullaryVoidMethod(B.class, "foo", appInfo.dexItemFactory());
+    ResolutionResult resolutionResult = appInfo.resolveMethod(method.holder, method);
+    DexType typeB = buildType(B.class, appInfo.dexItemFactory());
+    DexProgramClass classB = appInfo.definitionForProgramType(typeB);
+    LookupResult lookupResult =
+        resolutionResult.lookupVirtualDispatchTargets(
+            classB,
+            appView,
+            (type, subTypeConsumer, callSiteConsumer) -> {
+              if (type == typeB) {
+                subTypeConsumer.accept(classB);
+              }
+            },
+            reference -> false);
+    assertTrue(lookupResult.isLookupResultSuccess());
+    LookupResultSuccess lookupResultSuccess = lookupResult.asLookupResultSuccess();
+    Set<String> targets =
+        lookupResultSuccess.getMethodTargets().stream()
+            .map(DexEncodedMethod::qualifiedName)
+            .collect(Collectors.toSet());
+    Set<String> expected = ImmutableSet.of(A.class.getTypeName() + ".foo");
+    assertEquals(expected, targets);
+    assertTrue(lookupResultSuccess.isComplete());
+  }
+
+  @Test
+  public void testPrivateKeep() throws Exception {
+    // Unrelated { <-- kept
+    //   private foo() <-- kept
+    // }
+    AppView<AppInfoWithLiveness> appView =
+        computeAppViewWithLiveness(buildClasses(Unrelated.class).build(), Unrelated.class);
+    AppInfoWithLiveness appInfo = appView.appInfo();
+    DexMethod method = buildNullaryVoidMethod(Unrelated.class, "foo", appInfo.dexItemFactory());
+    ResolutionResult resolutionResult = appInfo.resolveMethod(method.holder, method);
+    DexProgramClass context =
+        appView.definitionForProgramType(buildType(Unrelated.class, appInfo.dexItemFactory()));
+    LookupResult lookupResult = resolutionResult.lookupVirtualDispatchTargets(context, appView);
+    assertTrue(lookupResult.isLookupResultSuccess());
+    LookupResultSuccess lookupResultSuccess = lookupResult.asLookupResultSuccess();
+    Set<String> targets =
+        lookupResultSuccess.getMethodTargets().stream()
+            .map(DexEncodedMethod::qualifiedName)
+            .collect(Collectors.toSet());
+    Set<String> expected = ImmutableSet.of(Unrelated.class.getTypeName() + ".foo");
+    assertEquals(expected, targets);
+    assertTrue(lookupResultSuccess.isIncomplete());
+  }
+
+  @Test
+  public void testInterfaceKept() throws Exception {
+    // I {
+    //   foo() <-- kept
+    // }
+    //
+    // A implements I {
+    //   foo() <-- resolved
+    // }
+    //
+    // B extends A { } <-- initial, kept
+    //
+    // C extends B {
+    //   foo()
+    // }
+    assertTrue(testLookup(B.class, I.class, B.class, A.class, C.class).isIncomplete());
+  }
+
+  @Test
+  public void testInterfaceKeptWithoutKeepInLookup() throws Exception {
+    // I {
+    //   foo() <-- kept
+    // }
+    //
+    // A implements I {
+    //   foo() <-- resolved
+    // }
+    //
+    // B extends A { } <-- initial
+    //
+    // C extends B {
+    //   foo()
+    // }
+    assertTrue(testLookup(B.class, I.class, Unrelated.class, A.class, C.class).isComplete());
+  }
+
+  @Test
+  public void testInterfaceKeptAndImplementedInSupType() throws Exception {
+    // X {
+    //   foo() <-- resolved
+    // }
+    //
+    // I {
+    //   foo() <-- kept
+    // }
+    //
+    // Y extends X implements I { } <-- initial
+    //
+    // Z extends Y { } <-- kept
+    assertTrue(
+        testLookup(
+                Y.class,
+                I.class,
+                Z.class,
+                Collections.singleton(X.class),
+                Arrays.asList(X.class, I.class, Y.class, Z.class))
+            .isIncomplete());
+  }
+
+  // TODO(b/148769279): We need to look at the call site to see if it overrides
+  //   a method that is kept.
+
+  public static class Unrelated {
+
+    private void foo() {
+      System.out.println("Unrelated.foo");
+    }
+  }
+
+  public interface I {
+    void foo();
+  }
+
+  public static class A implements I {
+    @Override
+    public void foo() {
+      System.out.println("A.foo");
+    }
+  }
+
+  public static class B extends A {}
+
+  public static class C extends B {
+    @Override
+    public void foo() {
+      System.out.println("C.foo");
+    }
+  }
+
+  public static class X {
+
+    public void foo() {
+      System.out.println("X.foo");
+    }
+  }
+
+  public static class Y extends X implements I {}
+
+  public static class Z extends Y {}
+}