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 2f5ab5e..aa2402c 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;
 
@@ -66,6 +67,10 @@
     return this.equals(other);
   }
 
+  OptionalBool isLessThanOrEqualTo(AndroidApiLevel other);
+
+  OptionalBool isLessThanOrEqualTo(ComputedApiLevel other);
+
   class NotSetApiLevel implements ComputedApiLevel {
 
     private static final NotSetApiLevel INSTANCE = new NotSetApiLevel();
@@ -73,6 +78,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;
     }
@@ -95,6 +112,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;
     }
@@ -141,6 +168,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 1bc719c..134163d 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
@@ -6,6 +6,7 @@
 
 import static com.android.tools.r8.utils.MapUtils.ignoreKey;
 
+import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -35,6 +36,7 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.KeepFieldInfo;
 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.BooleanUtils;
 import com.android.tools.r8.utils.IntBox;
@@ -109,6 +111,7 @@
   }
 
   private final AppView<AppInfoWithLiveness> appView;
+  private final AndroidApiLevelCompute androidApiLevelCompute;
   private final ImmediateProgramSubtypingInfo immediateSubtypingInfo;
   private final Map<Set<DexProgramClass>, DexMethodSignatureSet> interfaceDispatchOutsideProgram;
 
@@ -120,6 +123,7 @@
       ImmediateProgramSubtypingInfo immediateSubtypingInfo,
       Map<Set<DexProgramClass>, DexMethodSignatureSet> interfaceDispatchOutsideProgram) {
     this.appView = appView;
+    this.androidApiLevelCompute = AndroidApiLevelCompute.create(appView);
     this.immediateSubtypingInfo = immediateSubtypingInfo;
     this.interfaceDispatchOutsideProgram = interfaceDispatchOutsideProgram;
   }
@@ -486,8 +490,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);
@@ -496,17 +501,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 =
@@ -514,15 +519,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;
@@ -533,7 +538,7 @@
             newStaticFieldType =
                 dynamicUpperBoundClassType.getInterfaces().getSingleKnownInterface();
           } else {
-            return field.getType();
+            return staticType;
           }
         } else {
           newStaticFieldType = dynamicUpperBoundClassType.getClassType();
@@ -542,8 +547,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, androidApiLevelCompute)) {
+        return staticType;
       }
 
       return newStaticFieldType;
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 089ccce..4c453da 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java
@@ -4,11 +4,19 @@
 
 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.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;
 
@@ -65,6 +73,81 @@
       return false;
     }
     assert options.apiModelingOptions().enableApiCallerIdentification;
-    return apiLevel.asKnownApiLevel().getApiLevel().isLessThanOrEqualTo(options.getMinApiLevel());
+    return apiLevel.isLessThanOrEqualTo(options.getMinApiLevel()).isTrue();
+  }
+
+  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,
+      AndroidApiLevelCompute androidApiLevelCompute,
+      InternalOptions options) {
+    assert options.apiModelingOptions().enableApiCallerIdentification;
+    assert !isApiSafeForReference(newDefinition, androidApiLevelCompute, options)
+        : "Clients should first check if the definition is present on all apis since the min api";
+    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,
+      AndroidApiLevelCompute androidApiLevelCompute) {
+    // 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;
+    }
+    InternalOptions options = appView.options();
+    if (!options.apiModelingOptions().enableApiCallerIdentification) {
+      // Conservatively bail out if we don't have api modeling.
+      return false;
+    }
+    LibraryClass newBaseLibraryClass = newBaseClass.asLibraryClass();
+    if (isApiSafeForReference(newBaseLibraryClass, androidApiLevelCompute, options)) {
+      // 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, androidApiLevelCompute, options);
   }
 }
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..16e5c6c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelTypeStrengtheningAboveMinApiTest.java
@@ -0,0 +1,97 @@
+// 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)
+        .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..5df717d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelTypeStrengtheningTest.java
@@ -0,0 +1,101 @@
+// 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)
+        .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;
+    }
+  }
+}