Consult api database during type strengthening

Bug: b/236294139
Change-Id: I9e070782a2da544eba90b15c41d5b7281142cc3b
diff --git a/src/main/java/com/android/tools/r8/androidapi/ComputedApiLevel.java b/src/main/java/com/android/tools/r8/androidapi/ComputedApiLevel.java
index a2e9e87..3ff4dcb 100644
--- a/src/main/java/com/android/tools/r8/androidapi/ComputedApiLevel.java
+++ b/src/main/java/com/android/tools/r8/androidapi/ComputedApiLevel.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.androidapi;
 
 import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.OptionalBool;
 import com.android.tools.r8.utils.structural.Equatable;
 import java.util.Objects;
 
@@ -70,6 +71,10 @@
     return this.equals(other);
   }
 
+  OptionalBool isLessThanOrEqualTo(AndroidApiLevel other);
+
+  OptionalBool isLessThanOrEqualTo(ComputedApiLevel other);
+
   class NotSetApiLevel implements ComputedApiLevel {
 
     private static final NotSetApiLevel INSTANCE = new NotSetApiLevel();
@@ -77,6 +82,18 @@
     private NotSetApiLevel() {}
 
     @Override
+    public OptionalBool isLessThanOrEqualTo(AndroidApiLevel other) {
+      assert false : "Cannot compute relationship for not set";
+      return OptionalBool.unknown();
+    }
+
+    @Override
+    public OptionalBool isLessThanOrEqualTo(ComputedApiLevel other) {
+      assert false : "Cannot compute relationship for not set";
+      return OptionalBool.unknown();
+    }
+
+    @Override
     public boolean isNotSetApiLevel() {
       return true;
     }
@@ -99,6 +116,16 @@
     private UnknownApiLevel() {}
 
     @Override
+    public OptionalBool isLessThanOrEqualTo(AndroidApiLevel other) {
+      return OptionalBool.unknown();
+    }
+
+    @Override
+    public OptionalBool isLessThanOrEqualTo(ComputedApiLevel other) {
+      return OptionalBool.unknown();
+    }
+
+    @Override
     public boolean isUnknownApiLevel() {
       return true;
     }
@@ -145,6 +172,20 @@
     }
 
     @Override
+    public OptionalBool isLessThanOrEqualTo(AndroidApiLevel other) {
+      return OptionalBool.of(apiLevel.isLessThanOrEqualTo(other));
+    }
+
+    @Override
+    public OptionalBool isLessThanOrEqualTo(ComputedApiLevel other) {
+      if (other.isKnownApiLevel()) {
+        return isLessThanOrEqualTo(other.asKnownApiLevel().getApiLevel());
+      }
+      assert other.isUnknownApiLevel() : "Cannot compute relationship for not set";
+      return OptionalBool.unknown();
+    }
+
+    @Override
     public String toString() {
       return apiLevel.toString();
     }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorProgramOptimizer.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorProgramOptimizer.java
index bd3fee8..df0f324 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorProgramOptimizer.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorProgramOptimizer.java
@@ -40,6 +40,7 @@
 import com.android.tools.r8.shaking.KeepFieldInfo;
 import com.android.tools.r8.shaking.KeepMethodInfo;
 import com.android.tools.r8.utils.AccessUtils;
+import com.android.tools.r8.utils.AndroidApiLevelUtils;
 import com.android.tools.r8.utils.BooleanBox;
 import com.android.tools.r8.utils.IntBox;
 import com.android.tools.r8.utils.InternalOptions;
@@ -642,8 +643,9 @@
 
     private DexType getNewFieldType(ProgramField field) {
       DynamicType dynamicType = field.getOptimizationInfo().getDynamicType();
+      DexType staticType = field.getType();
       if (dynamicType.isUnknown()) {
-        return field.getType();
+        return staticType;
       }
 
       KeepFieldInfo keepInfo = appView.getKeepInfo(field);
@@ -652,17 +654,17 @@
       assert !keepInfo.isPinned(options);
 
       if (!keepInfo.isFieldTypeStrengtheningAllowed(options)) {
-        return field.getType();
+        return staticType;
       }
 
       if (dynamicType.isNullType()) {
         // Don't optimize always null fields; these will be optimized anyway.
-        return field.getType();
+        return staticType;
       }
 
       if (dynamicType.isNotNullType()) {
         // We don't have a more specific type.
-        return field.getType();
+        return staticType;
       }
 
       DynamicTypeWithUpperBound dynamicTypeWithUpperBound =
@@ -670,15 +672,15 @@
       TypeElement dynamicUpperBoundType = dynamicTypeWithUpperBound.getDynamicUpperBoundType();
       assert dynamicUpperBoundType.isReferenceType();
 
-      ClassTypeElement staticFieldType = field.getType().toTypeElement(appView).asClassType();
+      ClassTypeElement staticFieldType = staticType.toTypeElement(appView).asClassType();
       if (dynamicUpperBoundType.equalUpToNullability(staticFieldType)) {
         // We don't have more precise type information.
-        return field.getType();
+        return staticType;
       }
 
       if (!dynamicUpperBoundType.strictlyLessThan(staticFieldType, appView)) {
         assert options.testing.allowTypeErrors;
-        return field.getType();
+        return staticType;
       }
 
       DexType newStaticFieldType;
@@ -689,7 +691,7 @@
             newStaticFieldType =
                 dynamicUpperBoundClassType.getInterfaces().getSingleKnownInterface();
           } else {
-            return field.getType();
+            return staticType;
           }
         } else {
           newStaticFieldType = dynamicUpperBoundClassType.getClassType();
@@ -698,8 +700,17 @@
         newStaticFieldType = dynamicUpperBoundType.asArrayType().toDexType(dexItemFactory);
       }
 
-      if (!AccessUtils.isAccessibleInSameContextsAs(newStaticFieldType, field.getType(), appView)) {
-        return field.getType();
+      if (newStaticFieldType == staticType) {
+        return staticType;
+      }
+
+      if (!AccessUtils.isAccessibleInSameContextsAs(newStaticFieldType, staticType, appView)) {
+        return staticType;
+      }
+
+      if (!AndroidApiLevelUtils.isApiSafeForTypeStrengthening(
+          newStaticFieldType, staticType, appView)) {
+        return staticType;
       }
 
       return newStaticFieldType;
@@ -965,10 +976,13 @@
       if (!appView.appInfo().isSubtype(newReturnType, staticType)) {
         return null;
       }
-      return AccessUtils.isAccessibleInSameContextsAs(
-              newReturnType, method.getReturnType(), appView)
-          ? newReturnType
-          : null;
+      if (!AccessUtils.isAccessibleInSameContextsAs(newReturnType, staticType, appView)) {
+        return null;
+      }
+      if (!AndroidApiLevelUtils.isApiSafeForTypeStrengthening(newReturnType, staticType, appView)) {
+        return null;
+      }
+      return newReturnType;
     }
 
     private SingleValue getReturnValue(ProgramMethod method) {
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java b/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java
index 374459a..189d9f9 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java
@@ -4,12 +4,20 @@
 
 package com.android.tools.r8.utils;
 
+import static com.android.tools.r8.graph.DexLibraryClass.asLibraryClassOrNull;
+
 import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
 import com.android.tools.r8.androidapi.ComputedApiLevel;
+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;
+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.LibraryClass;
+import com.android.tools.r8.graph.LibraryDefinition;
 import com.android.tools.r8.graph.LibraryMethod;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.optimize.inliner.NopWhyAreYouNotInliningReporter;
@@ -95,11 +103,78 @@
     if (apiLevelOfOriginal.isUnknownApiLevel()) {
       return false;
     }
-    return apiLevelOfOriginal
-        .asKnownApiLevel()
-        .max(apiLevel)
-        .asKnownApiLevel()
-        .getApiLevel()
-        .isLessThanOrEqualTo(options.getMinApiLevel());
+    return apiLevelOfOriginal.max(apiLevel).isLessThanOrEqualTo(options.getMinApiLevel()).isTrue();
+  }
+
+  private static boolean isApiSafeForReference(LibraryDefinition definition, AppView<?> appView) {
+    return isApiSafeForReference(definition, appView.apiLevelCompute(), appView.options());
+  }
+
+  private static boolean isApiSafeForReference(
+      LibraryDefinition definition,
+      AndroidApiLevelCompute androidApiLevelCompute,
+      InternalOptions options) {
+    assert options.apiModelingOptions().enableApiCallerIdentification;
+    ComputedApiLevel apiLevel =
+        androidApiLevelCompute.computeApiLevelForLibraryReference(
+            definition.getReference(), ComputedApiLevel.unknown());
+    return apiLevel.isLessThanOrEqualTo(options.getMinApiLevel()).isTrue();
+  }
+
+  private static boolean isApiSafeForReference(
+      LibraryDefinition newDefinition, LibraryDefinition oldDefinition, AppView<?> appView) {
+    assert appView.options().apiModelingOptions().enableApiCallerIdentification;
+    assert !isApiSafeForReference(newDefinition, appView)
+        : "Clients should first check if the definition is present on all apis since the min api";
+    AndroidApiLevelCompute androidApiLevelCompute = appView.apiLevelCompute();
+    ComputedApiLevel apiLevel =
+        androidApiLevelCompute.computeApiLevelForLibraryReference(
+            newDefinition.getReference(), ComputedApiLevel.unknown());
+    if (apiLevel.isUnknownApiLevel()) {
+      return false;
+    }
+    ComputedApiLevel apiLevelOfOriginal =
+        androidApiLevelCompute.computeApiLevelForLibraryReference(
+            oldDefinition.getReference(), ComputedApiLevel.unknown());
+    return apiLevel.isLessThanOrEqualTo(apiLevelOfOriginal).isTrue();
+  }
+
+  public static boolean isApiSafeForTypeStrengthening(
+      DexType newType, DexType oldType, AppView<? extends AppInfoWithClassHierarchy> appView) {
+    // Type strengthening only applies to reference types.
+    assert newType.isReferenceType();
+    assert oldType.isReferenceType();
+    assert newType != oldType;
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    DexType newBaseType = newType.toBaseType(dexItemFactory);
+    if (newBaseType.isPrimitiveType()) {
+      // Array of primitives is available on all api levels.
+      return true;
+    }
+    assert newBaseType.isClassType();
+    DexClass newBaseClass = appView.definitionFor(newBaseType);
+    if (newBaseClass == null) {
+      // This could be a library class that is only available on newer api levels.
+      return false;
+    }
+    if (!newBaseClass.isLibraryClass()) {
+      // Program and classpath classes are not api level dependent.
+      return true;
+    }
+    if (!appView.options().apiModelingOptions().enableApiCallerIdentification) {
+      // Conservatively bail out if we don't have api modeling.
+      return false;
+    }
+    LibraryClass newBaseLibraryClass = newBaseClass.asLibraryClass();
+    if (isApiSafeForReference(newBaseLibraryClass, appView)) {
+      // Library class is present on all api levels since min api.
+      return true;
+    }
+    // Check if the new library class is present since the api level of the old type.
+    DexType oldBaseType = oldType.toBaseType(dexItemFactory);
+    assert oldBaseType.isClassType();
+    LibraryClass oldBaseLibraryClass = asLibraryClassOrNull(appView.definitionFor(oldBaseType));
+    return oldBaseLibraryClass != null
+        && isApiSafeForReference(newBaseLibraryClass, oldBaseLibraryClass, appView);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelTypeStrengtheningAboveMinApiTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelTypeStrengtheningAboveMinApiTest.java
new file mode 100644
index 0000000..d974c7a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelTypeStrengtheningAboveMinApiTest.java
@@ -0,0 +1,98 @@
+// 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.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.FieldSubject;
+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 ApiModelTypeStrengtheningAboveMinApiTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    int sdkInt = parameters.isCfRuntime() ? 0 : parameters.getApiLevel().getLevel();
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class, Version.class)
+        .addLibraryClasses(ApiLevel22.class, ApiLevel23.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .addKeepMainRule(Main.class)
+        .addKeepRules(
+            "-assumevalues class " + Version.class.getTypeName() + " {",
+            "  public static int getSdkInt(int) return " + sdkInt + "..42;",
+            "}")
+        .apply(setMockApiLevelForClass(ApiLevel22.class, AndroidApiLevel.L_MR1))
+        .apply(setMockApiLevelForClass(ApiLevel23.class, AndroidApiLevel.M))
+        .apply(ApiModelingTestHelper::enableApiCallerIdentification)
+        .apply(ApiModelingTestHelper::disableOutliningAndStubbing)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              FieldSubject fieldSubject = inspector.clazz(Main.class).uniqueFieldWithName("FIELD");
+              assertThat(fieldSubject, isPresent());
+              assertEquals(
+                  ApiLevel22.class.getTypeName(), fieldSubject.getField().getType().getTypeName());
+            })
+        .addRunClasspathClasses(ApiLevel22.class, ApiLevel23.class)
+        .run(parameters.getRuntime(), Main.class, Integer.toString(sdkInt))
+        .applyIf(
+            parameters.isDexRuntime()
+                && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.L_MR1),
+            runResult -> runResult.assertSuccessWithOutputLines("ApiLevel22"),
+            runResult -> runResult.assertSuccessWithOutputLines("null"));
+  }
+
+  public static class ApiLevel23 {}
+
+  public static class ApiLevel22 extends ApiLevel23 {
+
+    @Override
+    public String toString() {
+      return "ApiLevel22";
+    }
+  }
+
+  public static class Main {
+
+    public static ApiLevel23 FIELD;
+
+    public static void main(String[] args) {
+      int sdk = Integer.parseInt(args[0]);
+      if (Version.getSdkInt(sdk) >= 22) {
+        FIELD = new ApiLevel22();
+      }
+      System.out.println(FIELD);
+    }
+  }
+
+  public static class Version {
+
+    // -assumevalues ...
+    public static int getSdkInt(int sdk) {
+      return sdk;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelTypeStrengtheningTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelTypeStrengtheningTest.java
new file mode 100644
index 0000000..1d17e5c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelTypeStrengtheningTest.java
@@ -0,0 +1,102 @@
+// 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.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.FieldSubject;
+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 ApiModelTypeStrengtheningTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    boolean isTypeStrengtheningSafe =
+        parameters.isDexRuntime()
+            && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.M);
+    int sdkInt = parameters.isCfRuntime() ? 0 : parameters.getApiLevel().getLevel();
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class, Version.class)
+        .addLibraryClasses(ApiLevel22.class, ApiLevel23.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .addKeepMainRule(Main.class)
+        .addKeepRules(
+            "-assumevalues class " + Version.class.getTypeName() + " {",
+            "  public static int getSdkInt(int) return " + sdkInt + "..42;",
+            "}")
+        .apply(setMockApiLevelForClass(ApiLevel22.class, AndroidApiLevel.L_MR1))
+        .apply(setMockApiLevelForClass(ApiLevel23.class, AndroidApiLevel.M))
+        .apply(ApiModelingTestHelper::enableApiCallerIdentification)
+        .apply(ApiModelingTestHelper::disableOutliningAndStubbing)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              Class<?> expectedFieldType =
+                  isTypeStrengtheningSafe ? ApiLevel23.class : ApiLevel22.class;
+              FieldSubject fieldSubject = inspector.clazz(Main.class).uniqueFieldWithName("FIELD");
+              assertThat(fieldSubject, isPresent());
+              assertEquals(
+                  expectedFieldType.getTypeName(), fieldSubject.getField().getType().getTypeName());
+            })
+        .addRunClasspathClasses(ApiLevel22.class, ApiLevel23.class)
+        .run(parameters.getRuntime(), Main.class, Integer.toString(sdkInt))
+        .applyIf(
+            isTypeStrengtheningSafe,
+            runResult -> runResult.assertSuccessWithOutputLines("ApiLevel23"),
+            runResult -> runResult.assertSuccessWithOutputLines("null"));
+  }
+
+  public static class ApiLevel22 {}
+
+  public static class ApiLevel23 extends ApiLevel22 {
+
+    @Override
+    public String toString() {
+      return "ApiLevel23";
+    }
+  }
+
+  public static class Main {
+
+    public static ApiLevel22 FIELD;
+
+    public static void main(String[] args) {
+      int sdk = Integer.parseInt(args[0]);
+      if (Version.getSdkInt(sdk) >= 23) {
+        FIELD = new ApiLevel23();
+      }
+      System.out.println(FIELD);
+    }
+  }
+
+  public static class Version {
+
+    // -assumevalues ...
+    public static int getSdkInt(int sdk) {
+      return sdk;
+    }
+  }
+}