[ApiModel] Outline static and instance field calls

Bug: b/242078642
Change-Id: I9d5a618baac7e9c6b508e020939f8a6bb42f20a7
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedField.java b/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
index 1aa41dc..f6edecd 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
@@ -221,10 +221,6 @@
     return accessFlags.isProtected();
   }
 
-  public boolean isPublic() {
-    return accessFlags.isPublic();
-  }
-
   @Override
   public boolean isStaticMember() {
     return isStatic();
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMember.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMember.java
index 7f42e49..806b7a7 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMember.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMember.java
@@ -73,6 +73,10 @@
     return getAccessFlags().isPrivate();
   }
 
+  public final boolean isPublic() {
+    return getAccessFlags().isPublic();
+  }
+
   public abstract ProgramMember<D, R> asProgramMember(DexDefinitionSupplier definitions);
 
   public abstract <T> T apply(
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 ba712ee..05b5030 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -443,10 +443,6 @@
     return accessFlags.isNative();
   }
 
-  public boolean isPublic() {
-    return accessFlags.isPublic();
-  }
-
   public boolean isSynchronized() {
     return accessFlags.isSynchronized();
   }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/apimodel/ApiInvokeOutlinerDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/apimodel/ApiInvokeOutlinerDesugaring.java
index 3c0046f..3050bdc 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/apimodel/ApiInvokeOutlinerDesugaring.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/apimodel/ApiInvokeOutlinerDesugaring.java
@@ -8,17 +8,21 @@
 
 import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
 import com.android.tools.r8.androidapi.ComputedApiLevel;
+import com.android.tools.r8.cf.code.CfFieldInstruction;
 import com.android.tools.r8.cf.code.CfInstruction;
 import com.android.tools.r8.cf.code.CfInvoke;
 import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
 import com.android.tools.r8.contexts.CompilationContext.UniqueContext;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedMember;
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexLibraryClass;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ProgramMethod;
@@ -27,11 +31,16 @@
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.FreshLocalProvider;
 import com.android.tools.r8.ir.desugar.LocalStackAllocator;
+import com.android.tools.r8.ir.synthetic.FieldAccessorBuilder;
 import com.android.tools.r8.ir.synthetic.ForwardMethodBuilder;
+import com.android.tools.r8.synthesis.SyntheticMethodBuilder;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.TraversalContinuation;
 import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.List;
+import java.util.function.Function;
 
 /**
  * This desugaring will outline calls to library methods that are introduced after the min-api
@@ -57,11 +66,12 @@
       MethodProcessingContext methodProcessingContext,
       CfInstructionDesugaringCollection desugaringCollection,
       DexItemFactory dexItemFactory) {
-    ComputedApiLevel computedApiLevel = getComputedApiLevelForMethodOnHolderWithMinApi(instruction);
+    ComputedApiLevel computedApiLevel =
+        getComputedApiLevelInstructionOnHolderWithMinApi(instruction);
     if (computedApiLevel.isGreaterThan(appView.computedMinApiLevel())) {
       return desugarLibraryCall(
           methodProcessingContext.createUniqueContext(),
-          instruction.asInvoke(),
+          instruction,
           computedApiLevel,
           dexItemFactory,
           eventConsumer,
@@ -75,59 +85,68 @@
     if (context.getDefinition().isD8R8Synthesized()) {
       return false;
     }
-    return getComputedApiLevelForMethodOnHolderWithMinApi(instruction)
+    return getComputedApiLevelInstructionOnHolderWithMinApi(instruction)
         .isGreaterThan(appView.computedMinApiLevel());
   }
 
-  private ComputedApiLevel getComputedApiLevelForMethodOnHolderWithMinApi(
+  private ComputedApiLevel getComputedApiLevelInstructionOnHolderWithMinApi(
       CfInstruction instruction) {
-    if (!instruction.isInvoke()) {
+    if (!instruction.isInvoke() && !instruction.isFieldInstruction()) {
       return appView.computedMinApiLevel();
     }
-    CfInvoke cfInvoke = instruction.asInvoke();
-    if (cfInvoke.isInvokeSpecial()) {
+    DexReference reference;
+    if (instruction.isInvoke()) {
+      CfInvoke cfInvoke = instruction.asInvoke();
+      if (cfInvoke.isInvokeSpecial()) {
+        return appView.computedMinApiLevel();
+      }
+      reference = cfInvoke.getMethod();
+    } else {
+      reference = instruction.asFieldInstruction().getField();
+    }
+    if (!reference.getContextType().isClassType()) {
       return appView.computedMinApiLevel();
     }
-    DexType holderType = cfInvoke.getMethod().getHolderType();
-    if (!holderType.isClassType()) {
-      return appView.computedMinApiLevel();
-    }
-    DexClass holder = appView.definitionFor(holderType);
+    DexClass holder = appView.definitionFor(reference.getContextType());
     if (holder == null || !holder.isLibraryClass()) {
       return appView.computedMinApiLevel();
     }
-    ComputedApiLevel methodApiLevel =
-        apiLevelCompute.computeApiLevelForLibraryReference(
-            cfInvoke.getMethod(), ComputedApiLevel.unknown());
-    if (appView.computedMinApiLevel().isGreaterThanOrEqualTo(methodApiLevel)
-        || isApiLevelLessThanOrEqualTo9(methodApiLevel)
-        || methodApiLevel.isUnknownApiLevel()) {
+    ComputedApiLevel referenceApiLevel =
+        apiLevelCompute.computeApiLevelForLibraryReference(reference, ComputedApiLevel.unknown());
+    if (appView.computedMinApiLevel().isGreaterThanOrEqualTo(referenceApiLevel)
+        || isApiLevelLessThanOrEqualTo9(referenceApiLevel)
+        || referenceApiLevel.isUnknownApiLevel()) {
       return appView.computedMinApiLevel();
     }
     // Check for protected or package private access flags before outlining.
     if (holder.isInterface()) {
-      return methodApiLevel;
+      return referenceApiLevel;
     } else {
-      DexEncodedMethod methodDefinition =
-          simpleLookupInClassHierarchy(holder.asLibraryClass(), cfInvoke.getMethod());
-      return methodDefinition != null && methodDefinition.isPublic()
-          ? methodApiLevel
+      DexEncodedMember<?, ?> definition =
+          simpleLookupInClassHierarchy(
+              holder.asLibraryClass(),
+              reference.isDexMethod()
+                  ? x -> x.lookupMethod(reference.asDexMethod())
+                  : x -> x.lookupField(reference.asDexField()));
+      return definition != null && definition.isPublic()
+          ? referenceApiLevel
           : appView.computedMinApiLevel();
     }
   }
 
-  private DexEncodedMethod simpleLookupInClassHierarchy(DexLibraryClass holder, DexMethod method) {
-    DexEncodedMethod result = holder.lookupMethod(method);
+  private DexEncodedMember<?, ?> simpleLookupInClassHierarchy(
+      DexLibraryClass holder, Function<DexClass, DexEncodedMember<?, ?>> lookup) {
+    DexEncodedMember<?, ?> result = lookup.apply(holder);
     if (result != null) {
       return result;
     }
-    TraversalContinuation<DexEncodedMethod, ?> traversalResult =
+    TraversalContinuation<DexEncodedMember<?, ?>, ?> traversalResult =
         appView
             .appInfoForDesugaring()
             .traverseSuperClasses(
                 holder,
                 (ignored, superClass, ignored_) -> {
-                  DexEncodedMethod definition = superClass.lookupMethod(method);
+                  DexEncodedMember<?, ?> definition = lookup.apply(superClass);
                   if (definition != null) {
                     return TraversalContinuation.doBreak(definition);
                   }
@@ -143,29 +162,24 @@
 
   private Collection<CfInstruction> desugarLibraryCall(
       UniqueContext uniqueContext,
-      CfInvoke invoke,
+      CfInstruction instruction,
       ComputedApiLevel computedApiLevel,
       DexItemFactory factory,
       ApiInvokeOutlinerDesugaringEventConsumer eventConsumer,
       ProgramMethod context) {
-    DexMethod method = invoke.getMethod();
+    assert instruction.isInvoke() || instruction.isFieldInstruction();
     ProgramMethod outlinedMethod =
-        ensureOutlineMethod(uniqueContext, method, computedApiLevel, factory, invoke);
+        ensureOutlineMethod(uniqueContext, instruction, computedApiLevel, factory, context);
     eventConsumer.acceptOutlinedMethod(outlinedMethod, context);
     return ImmutableList.of(new CfInvoke(INVOKESTATIC, outlinedMethod.getReference(), false));
   }
 
   private ProgramMethod ensureOutlineMethod(
       UniqueContext context,
-      DexMethod apiMethod,
+      CfInstruction instruction,
       ComputedApiLevel apiLevel,
       DexItemFactory factory,
-      CfInvoke invoke) {
-    DexClass libraryHolder = appView.definitionFor(apiMethod.getHolderType());
-    assert libraryHolder != null;
-    boolean isVirtualMethod = invoke.isInvokeVirtual() || invoke.isInvokeInterface();
-    assert verifyLibraryHolderAndInvoke(libraryHolder, apiMethod, isVirtualMethod);
-    DexProto proto = factory.prependHolderToProtoIf(apiMethod, isVirtualMethod);
+      ProgramMethod programContext) {
     return appView
         .getSyntheticItems()
         .createMethod(
@@ -174,7 +188,6 @@
             appView,
             syntheticMethodBuilder -> {
               syntheticMethodBuilder
-                  .setProto(proto)
                   .setAccessFlags(
                       MethodAccessFlags.builder()
                           .setPublic()
@@ -183,24 +196,83 @@
                           .setBridge()
                           .build())
                   .setApiLevelForDefinition(apiLevel)
-                  .setApiLevelForCode(apiLevel)
-                  .setCode(
-                      m -> {
-                        if (isVirtualMethod) {
-                          return ForwardMethodBuilder.builder(factory)
-                              .setVirtualTarget(apiMethod, libraryHolder.isInterface())
-                              .setNonStaticSource(apiMethod)
-                              .build();
-                        } else {
-                          return ForwardMethodBuilder.builder(factory)
-                              .setStaticTarget(apiMethod, libraryHolder.isInterface())
-                              .setStaticSource(apiMethod)
-                              .build();
-                        }
-                      });
+                  .setApiLevelForCode(apiLevel);
+              if (instruction.isInvoke()) {
+                setCodeForInvoke(syntheticMethodBuilder, instruction.asInvoke(), factory);
+              } else {
+                assert instruction.isCfInstruction();
+                setCodeForFieldInstruction(
+                    syntheticMethodBuilder,
+                    instruction.asFieldInstruction(),
+                    factory,
+                    programContext);
+              }
             });
   }
 
+  private void setCodeForInvoke(
+      SyntheticMethodBuilder methodBuilder, CfInvoke invoke, DexItemFactory factory) {
+    DexMethod method = invoke.getMethod();
+    DexClass libraryHolder = appView.definitionFor(method.getHolderType());
+    assert libraryHolder != null;
+    boolean isVirtualMethod = invoke.isInvokeVirtual() || invoke.isInvokeInterface();
+    assert verifyLibraryHolderAndInvoke(libraryHolder, method, isVirtualMethod);
+    DexProto proto = factory.prependHolderToProtoIf(method, isVirtualMethod);
+    methodBuilder
+        .setProto(proto)
+        .setCode(
+            m -> {
+              if (isVirtualMethod) {
+                return ForwardMethodBuilder.builder(factory)
+                    .setVirtualTarget(method, libraryHolder.isInterface())
+                    .setNonStaticSource(method)
+                    .build();
+              } else {
+                return ForwardMethodBuilder.builder(factory)
+                    .setStaticTarget(method, libraryHolder.isInterface())
+                    .setStaticSource(method)
+                    .build();
+              }
+            });
+  }
+
+  private void setCodeForFieldInstruction(
+      SyntheticMethodBuilder methodBuilder,
+      CfFieldInstruction fieldInstruction,
+      DexItemFactory factory,
+      ProgramMethod programContext) {
+    DexField field = fieldInstruction.getField();
+    DexClass libraryHolder = appView.definitionFor(field.getHolderType());
+    assert libraryHolder != null;
+    boolean isInstance =
+        fieldInstruction.isInstanceFieldPut() || fieldInstruction.isInstanceFieldGet();
+    // Outlined field references will return a value if getter and only takes arguments if
+    // instance or if put or two arguments if both.
+    DexType returnType = fieldInstruction.isFieldGet() ? field.getType() : factory.voidType;
+    List<DexType> parameters = new ArrayList<>();
+    if (isInstance) {
+      parameters.add(libraryHolder.getType());
+    }
+    if (fieldInstruction.isFieldPut()) {
+      parameters.add(field.getType());
+    }
+    methodBuilder
+        .setProto(factory.createProto(returnType, parameters))
+        .setCode(
+            m ->
+                FieldAccessorBuilder.builder()
+                    .applyIf(
+                        isInstance,
+                        thenConsumer -> thenConsumer.setInstanceField(field),
+                        elseConsumer -> elseConsumer.setStaticField(field))
+                    .applyIf(
+                        fieldInstruction.isFieldGet(),
+                        FieldAccessorBuilder::setGetter,
+                        FieldAccessorBuilder::setSetter)
+                    .setSourceMethod(programContext.getReference())
+                    .build());
+  }
+
   private boolean verifyLibraryHolderAndInvoke(
       DexClass libraryHolder, DexMethod apiMethod, boolean isVirtualInvoke) {
     DexEncodedMethod libraryApiMethodDefinition = libraryHolder.lookupMethod(apiMethod);
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineInstanceFieldTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineInstanceFieldTest.java
index a5832ff..2d9de46 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineInstanceFieldTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineInstanceFieldTest.java
@@ -7,6 +7,7 @@
 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.setMockApiLevelForField;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.verifyThat;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.CompilationMode;
@@ -17,6 +18,8 @@
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.testing.AndroidBuildVersion;
 import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.lang.reflect.Field;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -29,7 +32,8 @@
   private static final AndroidApiLevel classApiLevel = AndroidApiLevel.M;
   private static final AndroidApiLevel fieldApiLevel = AndroidApiLevel.P;
 
-  private static final String EXPECTED = "LibraryClass::field";
+  private static final String[] EXPECTED =
+      new String[] {"LibraryClass::getField", "LibraryClass::putField"};
 
   @Parameter public TestParameters parameters;
 
@@ -47,7 +51,8 @@
         .addAndroidBuildVersion()
         .apply(setMockApiLevelForClass(LibraryClass.class, classApiLevel))
         .apply(setMockApiLevelForDefaultInstanceInitializer(LibraryClass.class, classApiLevel))
-        .apply(setMockApiLevelForField(LibraryClass.class.getField("field"), fieldApiLevel))
+        .apply(setMockApiLevelForField(LibraryClass.class.getField("getField"), fieldApiLevel))
+        .apply(setMockApiLevelForField(LibraryClass.class.getField("putField"), fieldApiLevel))
         .apply(ApiModelingTestHelper::enableApiCallerIdentification)
         .apply(ApiModelingTestHelper::enableOutliningOfMethods)
         .apply(ApiModelingTestHelper::disableStubbingOfClasses);
@@ -65,8 +70,7 @@
         .setMode(CompilationMode.DEBUG)
         .apply(this::setupTestBuilder)
         .compile()
-        // TODO(b/242078642):  We should outline field references to newer api levels.
-        .inspect(ApiModelingTestHelper::assertNoSynthesizedClasses)
+        .inspect(this::inspect)
         .applyIf(addToBootClasspath(), b -> b.addBootClasspathClasses(LibraryClass.class))
         .run(parameters.getRuntime(), Main.class)
         .apply(this::checkOutput);
@@ -79,8 +83,7 @@
         .setMode(CompilationMode.RELEASE)
         .apply(this::setupTestBuilder)
         .compile()
-        // TODO(b/242078642):  We should outline field references to newer api levels.
-        .inspect(ApiModelingTestHelper::assertNoSynthesizedClasses)
+        .inspect(this::inspect)
         .applyIf(addToBootClasspath(), b -> b.addBootClasspathClasses(LibraryClass.class))
         .run(parameters.getRuntime(), Main.class)
         .apply(this::checkOutput);
@@ -92,13 +95,23 @@
         .apply(this::setupTestBuilder)
         .addKeepMainRule(Main.class)
         .compile()
-        // TODO(b/242078642):  We should outline field references to newer api levels.
-        .inspect(ApiModelingTestHelper::assertNoSynthesizedClasses)
+        .inspect(this::inspect)
         .applyIf(addToBootClasspath(), b -> b.addBootClasspathClasses(LibraryClass.class))
         .run(parameters.getRuntime(), Main.class)
         .apply(this::checkOutput);
   }
 
+  private void inspect(CodeInspector inspector) throws Exception {
+    Field[] fields =
+        new Field[] {
+          LibraryClass.class.getField("getField"), LibraryClass.class.getField("putField")
+        };
+    for (Field field : fields) {
+      verifyThat(inspector, parameters, field)
+          .isOutlinedFromUntil(Main.class.getMethod("main", String[].class), fieldApiLevel);
+    }
+  }
+
   private void checkOutput(SingleTestRunResult<?> runResult) {
     if (parameters.isDexRuntime()
         && parameters.getApiLevel().isGreaterThanOrEqualTo(classApiLevel)) {
@@ -111,7 +124,9 @@
   // Only present from api level 23.
   public static class LibraryClass {
 
-    public String field = "LibraryClass::field";
+    public String getField = "LibraryClass::getField";
+
+    public String putField;
   }
 
   public static class Main {
@@ -119,7 +134,9 @@
     public static void main(String[] args) {
       if (AndroidBuildVersion.VERSION >= 23) {
         LibraryClass libraryClass = new LibraryClass();
-        System.out.println(libraryClass.field);
+        System.out.println(libraryClass.getField);
+        libraryClass.putField = "LibraryClass::putField";
+        System.out.println(libraryClass.putField);
       } else {
         System.out.println("Not calling API");
       }
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineStaticFieldTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineStaticFieldTest.java
index d00685f..79b2cb8 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineStaticFieldTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineStaticFieldTest.java
@@ -6,6 +6,7 @@
 
 import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
 import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForField;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.verifyThat;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.CompilationMode;
@@ -16,6 +17,8 @@
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.testing.AndroidBuildVersion;
 import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.lang.reflect.Field;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -28,7 +31,8 @@
   private static final AndroidApiLevel classApiLevel = AndroidApiLevel.M;
   private static final AndroidApiLevel fieldApiLevel = AndroidApiLevel.P;
 
-  private static final String EXPECTED = "LibraryClass::field";
+  private static final String[] EXPECTED =
+      new String[] {"LibraryClass::getField", "LibraryClass::putField"};
 
   @Parameter public TestParameters parameters;
 
@@ -45,7 +49,8 @@
         .setMinApi(parameters.getApiLevel())
         .addAndroidBuildVersion()
         .apply(setMockApiLevelForClass(LibraryClass.class, classApiLevel))
-        .apply(setMockApiLevelForField(LibraryClass.class.getField("field"), fieldApiLevel))
+        .apply(setMockApiLevelForField(LibraryClass.class.getField("getField"), fieldApiLevel))
+        .apply(setMockApiLevelForField(LibraryClass.class.getField("putField"), fieldApiLevel))
         .apply(ApiModelingTestHelper::enableApiCallerIdentification)
         .apply(ApiModelingTestHelper::enableOutliningOfMethods)
         .apply(ApiModelingTestHelper::disableStubbingOfClasses);
@@ -63,8 +68,7 @@
         .setMode(CompilationMode.DEBUG)
         .apply(this::setupTestBuilder)
         .compile()
-        // TODO(b/242078642):  We should outline field references to newer api levels.
-        .inspect(ApiModelingTestHelper::assertNoSynthesizedClasses)
+        .inspect(this::inspect)
         .applyIf(addToBootClasspath(), b -> b.addBootClasspathClasses(LibraryClass.class))
         .run(parameters.getRuntime(), Main.class)
         .apply(this::checkOutput);
@@ -77,8 +81,7 @@
         .setMode(CompilationMode.RELEASE)
         .apply(this::setupTestBuilder)
         .compile()
-        // TODO(b/242078642):  We should outline field references to newer api levels.
-        .inspect(ApiModelingTestHelper::assertNoSynthesizedClasses)
+        .inspect(this::inspect)
         .applyIf(addToBootClasspath(), b -> b.addBootClasspathClasses(LibraryClass.class))
         .run(parameters.getRuntime(), Main.class)
         .apply(this::checkOutput);
@@ -90,13 +93,23 @@
         .apply(this::setupTestBuilder)
         .addKeepMainRule(Main.class)
         .compile()
-        // TODO(b/242078642):  We should outline field references to newer api levels.
-        .inspect(ApiModelingTestHelper::assertNoSynthesizedClasses)
+        .inspect(this::inspect)
         .applyIf(addToBootClasspath(), b -> b.addBootClasspathClasses(LibraryClass.class))
         .run(parameters.getRuntime(), Main.class)
         .apply(this::checkOutput);
   }
 
+  private void inspect(CodeInspector inspector) throws Exception {
+    Field[] fields =
+        new Field[] {
+          LibraryClass.class.getField("getField"), LibraryClass.class.getField("putField")
+        };
+    for (Field field : fields) {
+      verifyThat(inspector, parameters, field)
+          .isOutlinedFromUntil(Main.class.getMethod("main", String[].class), fieldApiLevel);
+    }
+  }
+
   private void checkOutput(SingleTestRunResult<?> runResult) {
     if (parameters.isDexRuntime()
         && parameters.getApiLevel().isGreaterThanOrEqualTo(classApiLevel)) {
@@ -109,14 +122,18 @@
   // Only present from api level 23.
   public static class LibraryClass {
 
-    public static String field = "LibraryClass::field";
+    public static String getField = "LibraryClass::getField";
+
+    public static String putField;
   }
 
   public static class Main {
 
     public static void main(String[] args) {
       if (AndroidBuildVersion.VERSION >= 23) {
-        System.out.println(LibraryClass.field);
+        System.out.println(LibraryClass.getField);
+        LibraryClass.putField = "LibraryClass::putField";
+        System.out.println(LibraryClass.putField);
       } else {
         System.out.println("Not calling API");
       }
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java
index ed2343a..4166e5e 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.apimodel;
 
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.accessesField;
 import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod;
 import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethodWithName;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
@@ -11,10 +12,12 @@
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 
 import com.android.tools.r8.TestCompilerBuilder;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ThrowableConsumer;
+import com.android.tools.r8.references.FieldReference;
 import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.utils.AndroidApiLevel;
@@ -193,6 +196,12 @@
         inspector, parameters, Reference.methodFromMethod(method));
   }
 
+  static ApiModelingFieldVerificationHelper verifyThat(
+      CodeInspector inspector, TestParameters parameters, Field field) {
+    return new ApiModelingFieldVerificationHelper(
+        inspector, parameters, Reference.fieldFromField(field));
+  }
+
   public static void assertNoSynthesizedClasses(CodeInspector inspector) {
     assertEquals(
         Collections.emptySet(),
@@ -224,6 +233,51 @@
     }
   }
 
+  public static class ApiModelingFieldVerificationHelper {
+
+    private final CodeInspector inspector;
+    private final FieldReference fieldOfInterest;
+    private final TestParameters parameters;
+
+    private ApiModelingFieldVerificationHelper(
+        CodeInspector inspector, TestParameters parameters, FieldReference fieldOfInterest) {
+      this.inspector = inspector;
+      this.fieldOfInterest = fieldOfInterest;
+      this.parameters = parameters;
+    }
+
+    void isOutlinedFromUntil(Method method, AndroidApiLevel apiLevel) {
+      if (parameters.isDexRuntime() && parameters.getApiLevel().isLessThan(apiLevel)) {
+        isOutlinedFrom(method);
+      } else {
+        isNotOutlinedFrom(method);
+      }
+    }
+
+    void isOutlinedFrom(Method method) {
+      // Check that the call is in a synthetic class.
+      List<FoundMethodSubject> outlinedMethod =
+          inspector.allClasses().stream()
+              .flatMap(clazz -> clazz.allMethods().stream())
+              .filter(
+                  methodSubject ->
+                      methodSubject.isSynthetic()
+                          && accessesField(fieldOfInterest).matches(methodSubject))
+              .collect(Collectors.toList());
+      assertFalse(outlinedMethod.isEmpty());
+      // Assert that method invokes the outline
+      MethodSubject caller = inspector.method(method);
+      assertThat(caller, isPresent());
+      assertThat(caller, invokesMethod(outlinedMethod.get(0)));
+    }
+
+    void isNotOutlinedFrom(Method method) {
+      MethodSubject caller = inspector.method(method);
+      assertThat(caller, isPresent());
+      assertThat(caller, accessesField(fieldOfInterest));
+    }
+  }
+
   public static class ApiModelingMethodVerificationHelper {
 
     private final CodeInspector inspector;
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
index 50a77fe..db36bbc 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
@@ -4,8 +4,8 @@
 
 package com.android.tools.r8.utils.codeinspector;
 
-import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.references.FieldReference;
 import java.util.Arrays;
 import java.util.List;
 import java.util.function.Predicate;
@@ -20,7 +20,10 @@
     if (!targetSubject.isPresent()) {
       throw new IllegalArgumentException();
     }
-    DexField target = targetSubject.getField().getReference();
+    return accessesField(targetSubject.getField().getReference().asFieldReference());
+  }
+
+  public static Matcher<MethodSubject> accessesField(FieldReference target) {
     return new TypeSafeMatcher<MethodSubject>() {
       @Override
       protected boolean matchesSafely(MethodSubject subject) {
@@ -35,7 +38,7 @@
 
       @Override
       public void describeTo(Description description) {
-        description.appendText("accesses field `" + target.toSourceString() + "`");
+        description.appendText("accesses field `" + target.toString() + "`");
       }
 
       @Override
@@ -232,7 +235,8 @@
     };
   }
 
-  public static Predicate<InstructionSubject> isFieldAccessWithTarget(DexField target) {
-    return instruction -> instruction.isFieldAccess() && instruction.getField() == target;
+  public static Predicate<InstructionSubject> isFieldAccessWithTarget(FieldReference target) {
+    return instruction ->
+        instruction.isFieldAccess() && instruction.getField().asFieldReference().equals(target);
   }
 }