[ApiModel] Only visit super hierarchy for library classes

Bug: b/246251214
Change-Id: I557a0cb9eea4f7340432b62fa230d325bc66c043
diff --git a/src/main/java/com/android/tools/r8/androidapi/ApiReferenceStubber.java b/src/main/java/com/android/tools/r8/androidapi/ApiReferenceStubber.java
index 6d8c09d..6754dde 100644
--- a/src/main/java/com/android/tools/r8/androidapi/ApiReferenceStubber.java
+++ b/src/main/java/com/android/tools/r8/androidapi/ApiReferenceStubber.java
@@ -6,6 +6,7 @@
 
 import static com.android.tools.r8.utils.MapUtils.ignoreKey;
 
+import com.android.tools.r8.dex.code.CfOrDexInstruction;
 import com.android.tools.r8.errors.MissingGlobalSyntheticsConsumerDiagnostic;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfo;
@@ -30,12 +31,18 @@
 import com.android.tools.r8.utils.WorkList;
 import com.google.common.collect.Sets;
 import java.util.Arrays;
+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;
 
+/**
+ * The only instructions we do not outline is constant classes, instance-of/checkcast and exception
+ * guards. For program classes we also visit super types if these are library otherwise we will
+ * visit the program super type's super types when visiting that program class.
+ */
 public class ApiReferenceStubber {
 
   private class ReferencesToApiLevelUseRegistry extends UseRegistry<ProgramMethod> {
@@ -46,52 +53,52 @@
 
     @Override
     public void registerInitClass(DexType type) {
-      checkReferenceToLibraryClass(type);
+      // Intentionally empty.
     }
 
     @Override
     public void registerInvokeVirtual(DexMethod method) {
-      checkReferenceToLibraryClass(method);
+      // Intentionally empty.
     }
 
     @Override
     public void registerInvokeDirect(DexMethod method) {
-      checkReferenceToLibraryClass(method);
+      // Intentionally empty.
     }
 
     @Override
     public void registerInvokeStatic(DexMethod method) {
-      checkReferenceToLibraryClass(method);
+      // Intentionally empty.
     }
 
     @Override
     public void registerInvokeInterface(DexMethod method) {
-      checkReferenceToLibraryClass(method);
+      // Intentionally empty.
     }
 
     @Override
     public void registerInvokeSuper(DexMethod method) {
-      checkReferenceToLibraryClass(method);
+      // Intentionally empty.
     }
 
     @Override
     public void registerInstanceFieldRead(DexField field) {
-      checkReferenceToLibraryClass(field);
+      // Intentionally empty.
     }
 
     @Override
     public void registerInstanceFieldWrite(DexField field) {
-      checkReferenceToLibraryClass(field);
+      // Intentionally empty.
     }
 
     @Override
     public void registerStaticFieldRead(DexField field) {
-      checkReferenceToLibraryClass(field);
+      // Intentionally empty.
     }
 
     @Override
     public void registerStaticFieldWrite(DexField field) {
-      checkReferenceToLibraryClass(field);
+      // Intentionally empty.
     }
 
     @Override
@@ -99,6 +106,29 @@
       checkReferenceToLibraryClass(type);
     }
 
+    @Override
+    public void registerInstanceOf(DexType type) {
+      checkReferenceToLibraryClass(type);
+    }
+
+    @Override
+    public void registerConstClass(
+        DexType type,
+        ListIterator<? extends CfOrDexInstruction> iterator,
+        boolean ignoreCompatRules) {
+      checkReferenceToLibraryClass(type);
+    }
+
+    @Override
+    public void registerCheckCast(DexType type, boolean ignoreCompatRules) {
+      checkReferenceToLibraryClass(type);
+    }
+
+    @Override
+    public void registerExceptionGuard(DexType guard) {
+      checkReferenceToLibraryClass(guard);
+    }
+
     private void checkReferenceToLibraryClass(DexReference reference) {
       DexType rewrittenType = appView.graphLens().lookupType(reference.getContextType());
       findReferencedLibraryClasses(rewrittenType, getContext().getContextClass());
@@ -158,7 +188,9 @@
         .isSyntheticOfKind(clazz.getType(), kinds -> kinds.API_MODEL_OUTLINE)) {
       return;
     }
-    findReferencedLibraryClasses(clazz.type, clazz);
+    clazz
+        .allImmediateSupertypes()
+        .forEach(superType -> findReferencedLibraryClasses(superType, clazz));
     clazz.forEachProgramMethodMatching(
         DexEncodedMethod::hasCode,
         method -> method.registerCodeReferences(new ReferencesToApiLevelUseRegistry(method)));
@@ -171,22 +203,20 @@
     WorkList<DexType> workList = WorkList.newIdentityWorkList(type, seenTypes);
     while (workList.hasNext()) {
       DexClass clazz = appView.definitionFor(workList.next());
-      if (clazz == null) {
+      if (clazz == null || !clazz.isLibraryClass()) {
         continue;
       }
-      if (clazz.isLibraryClass()) {
-        ComputedApiLevel androidApiLevel =
-            apiLevelCompute.computeApiLevelForLibraryReference(
-                clazz.type, ComputedApiLevel.unknown());
-        if (androidApiLevel.isGreaterThan(appView.computedMinApiLevel())
-            && !androidApiLevel.isUnknownApiLevel()) {
-          libraryClassesToMock.add(clazz.asLibraryClass());
-          referencingContexts
-              .computeIfAbsent(clazz.asLibraryClass(), ignoreKey(Sets::newConcurrentHashSet))
-              .add(context);
-        }
+      ComputedApiLevel androidApiLevel =
+          apiLevelCompute.computeApiLevelForLibraryReference(
+              clazz.type, ComputedApiLevel.unknown());
+      if (androidApiLevel.isGreaterThan(appView.computedMinApiLevel())
+          && androidApiLevel.isKnownApiLevel()) {
+        workList.addIfNotSeen(clazz.allImmediateSupertypes());
+        libraryClassesToMock.add(clazz.asLibraryClass());
+        referencingContexts
+            .computeIfAbsent(clazz.asLibraryClass(), ignoreKey(Sets::newConcurrentHashSet))
+            .add(context);
       }
-      workList.addIfNotSeen(clazz.allImmediateSupertypes());
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelMockClassInstanceInitTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelMockClassInstanceInitTest.java
deleted file mode 100644
index 4f31982..0000000
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelMockClassInstanceInitTest.java
+++ /dev/null
@@ -1,140 +0,0 @@
-// Copyright (c) 2021, 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.verifyThat;
-import static org.hamcrest.CoreMatchers.containsString;
-import static org.hamcrest.CoreMatchers.not;
-import static org.junit.Assume.assumeTrue;
-
-import com.android.tools.r8.CompilationMode;
-import com.android.tools.r8.NeverInline;
-import com.android.tools.r8.SingleTestRunResult;
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.TestCompilerBuilder;
-import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.ToolHelper.DexVm.Version;
-import com.android.tools.r8.utils.AndroidApiLevel;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
-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 ApiModelMockClassInstanceInitTest extends TestBase {
-
-  private final AndroidApiLevel mockLevel = AndroidApiLevel.M;
-
-  @Parameter public TestParameters parameters;
-
-  @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimesAndApiLevels().build();
-  }
-
-  private boolean isGreaterOrEqualToMockLevel() {
-    return parameters.isDexRuntime() && parameters.getApiLevel().isGreaterThanOrEqualTo(mockLevel);
-  }
-
-  private void setupTestBuilder(TestCompilerBuilder<?, ?, ?, ?, ?> testBuilder) {
-    testBuilder
-        .addProgramClasses(Main.class, TestClass.class)
-        .addLibraryClasses(LibraryClass.class)
-        .addDefaultRuntimeLibrary(parameters)
-        .setMinApi(parameters.getApiLevel())
-        .addAndroidBuildVersion()
-        .apply(ApiModelingTestHelper::enableStubbingOfClasses)
-        .apply(setMockApiLevelForClass(LibraryClass.class, mockLevel))
-        .apply(setMockApiLevelForDefaultInstanceInitializer(LibraryClass.class, mockLevel));
-  }
-
-  @Test
-  public void testD8Debug() throws Exception {
-    assumeTrue(parameters.isDexRuntime());
-    testForD8()
-        .setMode(CompilationMode.DEBUG)
-        .apply(this::setupTestBuilder)
-        .compile()
-        .applyIf(isGreaterOrEqualToMockLevel(), b -> b.addBootClasspathClasses(LibraryClass.class))
-        .run(parameters.getRuntime(), Main.class)
-        .apply(this::checkOutput)
-        .inspect(this::inspect);
-  }
-
-  @Test
-  public void testD8Release() throws Exception {
-    assumeTrue(parameters.isDexRuntime());
-    testForD8()
-        .setMode(CompilationMode.RELEASE)
-        .apply(this::setupTestBuilder)
-        .compile()
-        .applyIf(isGreaterOrEqualToMockLevel(), b -> b.addBootClasspathClasses(LibraryClass.class))
-        .run(parameters.getRuntime(), Main.class)
-        .apply(this::checkOutput)
-        .inspect(this::inspect);
-  }
-
-  @Test
-  public void testR8() throws Exception {
-    testForR8(parameters.getBackend())
-        .apply(this::setupTestBuilder)
-        .addKeepMainRule(Main.class)
-        .enableInliningAnnotations()
-        .compile()
-        .applyIf(isGreaterOrEqualToMockLevel(), b -> b.addBootClasspathClasses(LibraryClass.class))
-        .run(parameters.getRuntime(), Main.class)
-        .apply(this::checkOutput)
-        .inspect(this::inspect);
-  }
-
-  private void checkOutput(SingleTestRunResult<?> runResult) {
-    if (isGreaterOrEqualToMockLevel()) {
-      runResult.assertSuccessWithOutputLines("LibraryClass::foo");
-    } else {
-      runResult.assertSuccessWithOutputLines("NoClassDefFoundError");
-    }
-    runResult.applyIf(
-        !isGreaterOrEqualToMockLevel()
-            && parameters.isDexRuntime()
-            && parameters.getDexRuntimeVersion().isNewerThanOrEqual(Version.V7_0_0),
-        result -> result.assertStderrMatches(not(containsString("This dex file is invalid"))));
-  }
-
-  private void inspect(CodeInspector inspector) {
-    verifyThat(inspector, parameters, LibraryClass.class).stubbedUntil(mockLevel);
-  }
-
-  // Only present from api level 23.
-  public static class LibraryClass {
-
-    public void foo() {
-      System.out.println("LibraryClass::foo");
-    }
-  }
-
-  public static class TestClass {
-
-    @NeverInline
-    public static void test() {
-      try {
-        new LibraryClass().foo();
-      } catch (ExceptionInInitializerError | NoClassDefFoundError er) {
-        System.out.println("NoClassDefFoundError");
-      }
-    }
-  }
-
-  public static class Main {
-
-    public static void main(String[] args) {
-      TestClass.test();
-    }
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelMockClassLoadingTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelMockClassLoadingTest.java
index b9476a7..b80d79c 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelMockClassLoadingTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelMockClassLoadingTest.java
@@ -6,14 +6,19 @@
 
 import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
 import static com.android.tools.r8.apimodel.ApiModelingTestHelper.verifyThat;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.not;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestCompilerBuilder;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.DexVm;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import org.junit.Test;
@@ -27,7 +32,7 @@
 
   private final AndroidApiLevel mockLevel = AndroidApiLevel.M;
 
-  @Parameter() public TestParameters parameters;
+  @Parameter public TestParameters parameters;
 
   @Parameters(name = "{0}")
   public static TestParametersCollection data() {
@@ -40,7 +45,7 @@
 
   private void setupTestBuilder(TestCompilerBuilder<?, ?, ?, ?, ?> testBuilder) {
     testBuilder
-        .addProgramClasses(Main.class)
+        .addProgramClasses(Main.class, TestClass.class)
         .addLibraryClasses(LibraryClass.class)
         .addDefaultRuntimeLibrary(parameters)
         .setMinApi(parameters.getApiLevel())
@@ -55,10 +60,10 @@
         .setMode(CompilationMode.DEBUG)
         .apply(this::setupTestBuilder)
         .compile()
-        .inspect(this::inspect)
         .applyIf(isGreaterOrEqualToMockLevel(), b -> b.addBootClasspathClasses(LibraryClass.class))
         .run(parameters.getRuntime(), Main.class)
-        .apply(this::checkOutput);
+        .apply(this::checkOutput)
+        .inspect(this::inspect);
   }
 
   @Test
@@ -68,10 +73,10 @@
         .setMode(CompilationMode.RELEASE)
         .apply(this::setupTestBuilder)
         .compile()
-        .inspect(this::inspect)
         .applyIf(isGreaterOrEqualToMockLevel(), b -> b.addBootClasspathClasses(LibraryClass.class))
         .run(parameters.getRuntime(), Main.class)
-        .apply(this::checkOutput);
+        .apply(this::checkOutput)
+        .inspect(this::inspect);
   }
 
   @Test
@@ -79,35 +84,56 @@
     testForR8(parameters.getBackend())
         .apply(this::setupTestBuilder)
         .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
         .compile()
-        .inspect(this::inspect)
         .applyIf(isGreaterOrEqualToMockLevel(), b -> b.addBootClasspathClasses(LibraryClass.class))
         .run(parameters.getRuntime(), Main.class)
-        .apply(this::checkOutput);
+        .apply(this::checkOutput)
+        .inspect(this::inspect);
+  }
+
+  private void checkOutput(SingleTestRunResult<?> runResult) {
+    if (isGreaterOrEqualToMockLevel()) {
+      runResult.assertSuccessWithOutputLines("Hello World!");
+    } else if (parameters.isDexRuntime()
+        && parameters.asDexRuntime().getVm().isEqualTo(DexVm.ART_4_4_4_HOST)) {
+      runResult.assertSuccessWithOutputLines("ClassNotFoundException");
+    } else {
+      runResult.assertSuccessWithOutputLines("NoClassDefFoundError");
+    }
+    runResult.applyIf(
+        !isGreaterOrEqualToMockLevel()
+            && parameters.isDexRuntime()
+            && parameters.getDexRuntimeVersion().isNewerThanOrEqual(Version.V7_0_0),
+        result -> result.assertStderrMatches(not(containsString("This dex file is invalid"))));
   }
 
   private void inspect(CodeInspector inspector) {
     verifyThat(inspector, parameters, LibraryClass.class).stubbedUntil(mockLevel);
   }
 
-  private void checkOutput(SingleTestRunResult<?> runResult) {
-    runResult.assertSuccessWithOutputLinesIf(isGreaterOrEqualToMockLevel(), "Hello World");
-    runResult.assertFailureWithErrorThatThrowsIf(
-        !isGreaterOrEqualToMockLevel(), NoClassDefFoundError.class);
-  }
+  // Only present from api level 23.
+  public static class LibraryClass {}
 
-  // Only present form api level 23.
-  public static class LibraryClass {
+  public static class TestClass {
 
-    public void foo() {
-      System.out.println("Hello World");
+    @NeverInline
+    public static void test() {
+      try {
+        Class.forName(LibraryClass.class.getName());
+        System.out.println("Hello World!");
+      } catch (ExceptionInInitializerError | NoClassDefFoundError er) {
+        System.out.println("NoClassDefFoundError");
+      } catch (ClassNotFoundException e) {
+        System.out.println("ClassNotFoundException");
+      }
     }
   }
 
   public static class Main {
 
     public static void main(String[] args) {
-      new LibraryClass().foo();
+      TestClass.test();
     }
   }
 }