[KeepAnno] Add field member patterns.

Bug: b/248408342
Change-Id: I1b9895f33a6465d1057386dfc1b43b5d90b93178
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstants.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstants.java
index 24e49d1..5b6de6e 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstants.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstants.java
@@ -31,5 +31,6 @@
     public static final String DESCRIPTOR = getDescriptor(CLASS);
     public static final String classConstant = "classConstant";
     public static final String methodName = "methodName";
+    public static final String fieldName = "fieldName";
   }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
index e20f288..f87e32c 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
@@ -16,4 +16,6 @@
   String classTypeName() default "";
 
   String methodName() default "";
+
+  String fieldName() default "";
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
index 029ec75..cc6e4ee 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
@@ -8,6 +8,8 @@
 import com.android.tools.r8.keepanno.ast.KeepConsequences;
 import com.android.tools.r8.keepanno.ast.KeepEdge;
 import com.android.tools.r8.keepanno.ast.KeepEdgeException;
+import com.android.tools.r8.keepanno.ast.KeepFieldNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
 import com.android.tools.r8.keepanno.ast.KeepItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepItemPattern.Builder;
 import com.android.tools.r8.keepanno.ast.KeepMethodNamePattern;
@@ -143,6 +145,7 @@
     private final Parent<KeepTarget> parent;
     private KeepQualifiedClassNamePattern classNamePattern = null;
     private KeepMethodNamePattern methodName = null;
+    private KeepFieldNamePattern fieldName = null;
 
     public KeepTargetVisitor(Parent<KeepTarget> parent) {
       this.parent = parent;
@@ -158,6 +161,10 @@
         methodName = KeepMethodNamePattern.exact((String) value);
         return;
       }
+      if (name.equals(Target.fieldName) && value instanceof String) {
+        fieldName = KeepFieldNamePattern.exact((String) value);
+        return;
+      }
       super.visit(name, value);
     }
 
@@ -167,10 +174,16 @@
       if (classNamePattern != null) {
         itemBuilder.setClassPattern(classNamePattern);
       }
+      if (methodName != null && fieldName != null) {
+        throw new KeepEdgeException("Cannot define both a field and a method pattern.");
+      }
       if (methodName != null) {
         itemBuilder.setMemberPattern(
             KeepMethodPattern.builder().setNamePattern(methodName).build());
       }
+      if (fieldName != null) {
+        itemBuilder.setMemberPattern(KeepFieldPattern.builder().setNamePattern(fieldName).build());
+      }
       KeepTarget target = KeepTarget.builder().setItem(itemBuilder.build()).build();
       parent.accept(target);
     }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
index 8307265..0a0ccc8 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
@@ -7,6 +7,9 @@
 import com.android.tools.r8.keepanno.annotations.KeepConstants.Target;
 import com.android.tools.r8.keepanno.ast.KeepConsequences;
 import com.android.tools.r8.keepanno.ast.KeepEdge;
+import com.android.tools.r8.keepanno.ast.KeepEdgeException;
+import com.android.tools.r8.keepanno.ast.KeepFieldNamePattern.KeepFieldNameExactPattern;
+import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
 import com.android.tools.r8.keepanno.ast.KeepItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepMemberPattern;
 import com.android.tools.r8.keepanno.ast.KeepMethodNamePattern.KeepMethodNameExactPattern;
@@ -84,7 +87,31 @@
     if (memberPattern.isAll()) {
       throw new Unimplemented();
     }
-    KeepMethodPattern method = memberPattern.asMethod();
+    if (memberPattern.isMethod()) {
+      writeMethod(memberPattern.asMethod(), targetVisitor);
+    } else if (memberPattern.isField()) {
+      writeField(memberPattern.asField(), targetVisitor);
+    } else {
+      throw new KeepEdgeException("Unexpected member pattern: " + memberPattern);
+    }
+  }
+
+  private void writeField(KeepFieldPattern field, AnnotationVisitor targetVisitor) {
+    KeepFieldNameExactPattern exactFieldName = field.getNamePattern().asExact();
+    if (exactFieldName != null) {
+      targetVisitor.visit(Target.fieldName, exactFieldName.getName());
+    } else {
+      throw new Unimplemented();
+    }
+    if (!field.getAccessPattern().isAny()) {
+      throw new Unimplemented();
+    }
+    if (!field.getTypePattern().isAny()) {
+      throw new Unimplemented();
+    }
+  }
+
+  private void writeMethod(KeepMethodPattern method, AnnotationVisitor targetVisitor) {
     KeepMethodNameExactPattern exactMethodName = method.getNamePattern().asExact();
     if (exactMethodName != null) {
       targetVisitor.visit(Target.methodName, exactMethodName.getName());
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java
index 89be76b..b2da25a 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java
@@ -39,7 +39,12 @@
  *   UNQUALIFIED_CLASS_NAME_PATTERN ::= any | exact simple-class-name
  *   EXTENDS_PATTERN ::= any | QUALIFIED_CLASS_NAME_PATTERN
  *
- *   MEMBER_PATTERN ::= none | all | METHOD_PATTERN
+ *   MEMBER_PATTERN ::= none | all | FIELD_PATTERN | METHOD_PATTERN
+ *
+ *   FIELD_PATTERN
+ *     ::= FIELD_ACCESS_PATTERN
+ *           FIELD_TYPE_PATTERN
+ *           FIELD_NAME_PATTERN;
  *
  *   METHOD_PATTERN
  *     ::= METHOD_ACCESS_PATTERN
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldAccessPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldAccessPattern.java
new file mode 100644
index 0000000..056430b
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldAccessPattern.java
@@ -0,0 +1,43 @@
+// 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.keepanno.ast;
+
+// TODO: finish this.
+public abstract class KeepFieldAccessPattern {
+
+  public static KeepFieldAccessPattern any() {
+    return Any.getInstance();
+  }
+
+  public abstract boolean isAny();
+
+  private static class Any extends KeepFieldAccessPattern {
+
+    private static final Any INSTANCE = new Any();
+
+    private static Any getInstance() {
+      return INSTANCE;
+    }
+
+    @Override
+    public boolean isAny() {
+      return true;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      return this == obj;
+    }
+
+    @Override
+    public int hashCode() {
+      return System.identityHashCode(this);
+    }
+
+    @Override
+    public String toString() {
+      return "*";
+    }
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldNamePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldNamePattern.java
new file mode 100644
index 0000000..3067966
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldNamePattern.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.keepanno.ast;
+
+public abstract class KeepFieldNamePattern {
+
+  public static KeepFieldNamePattern any() {
+    return Any.getInstance();
+  }
+
+  public static KeepFieldNamePattern exact(String methodName) {
+    return new KeepFieldNameExactPattern(methodName);
+  }
+
+  private KeepFieldNamePattern() {}
+
+  public boolean isAny() {
+    return false;
+  }
+
+  public final boolean isExact() {
+    return asExact() != null;
+  }
+
+  public KeepFieldNameExactPattern asExact() {
+    return null;
+  }
+
+  private static class Any extends KeepFieldNamePattern {
+    private static final Any INSTANCE = new Any();
+
+    public static Any getInstance() {
+      return INSTANCE;
+    }
+
+    @Override
+    public boolean isAny() {
+      return true;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      return this == obj;
+    }
+
+    @Override
+    public int hashCode() {
+      return System.identityHashCode(this);
+    }
+
+    @Override
+    public String toString() {
+      return "*";
+    }
+  }
+
+  public static class KeepFieldNameExactPattern extends KeepFieldNamePattern {
+    private final String name;
+
+    public KeepFieldNameExactPattern(String name) {
+      assert name != null;
+      this.name = name;
+    }
+
+    @Override
+    public KeepFieldNameExactPattern asExact() {
+      return this;
+    }
+
+    public String getName() {
+      return name;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      KeepFieldNameExactPattern that = (KeepFieldNameExactPattern) o;
+      return name.equals(that.name);
+    }
+
+    @Override
+    public int hashCode() {
+      return name.hashCode();
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldPattern.java
new file mode 100644
index 0000000..7137033
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldPattern.java
@@ -0,0 +1,116 @@
+// 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.keepanno.ast;
+
+import java.util.Objects;
+
+public final class KeepFieldPattern extends KeepMemberPattern {
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+
+    private KeepFieldAccessPattern accessPattern = KeepFieldAccessPattern.any();
+    private KeepFieldNamePattern namePattern = null;
+    private KeepFieldTypePattern typePattern = KeepFieldTypePattern.any();
+
+    private Builder() {}
+
+    public Builder self() {
+      return this;
+    }
+
+    public Builder setAccessPattern(KeepFieldAccessPattern accessPattern) {
+      this.accessPattern = accessPattern;
+      return self();
+    }
+
+    public Builder setNamePattern(KeepFieldNamePattern namePattern) {
+      this.namePattern = namePattern;
+      return self();
+    }
+
+    public Builder setTypePattern(KeepFieldTypePattern typePattern) {
+      this.typePattern = typePattern;
+      return self();
+    }
+
+    public KeepFieldPattern build() {
+      if (namePattern == null) {
+        throw new KeepEdgeException("Field pattern must declare a name pattern");
+      }
+      return new KeepFieldPattern(accessPattern, namePattern, typePattern);
+    }
+  }
+
+  private final KeepFieldAccessPattern accessPattern;
+  private final KeepFieldNamePattern namePattern;
+  private final KeepFieldTypePattern typePattern;
+
+  private KeepFieldPattern(
+      KeepFieldAccessPattern accessPattern,
+      KeepFieldNamePattern namePattern,
+      KeepFieldTypePattern typePattern) {
+    assert accessPattern != null;
+    assert namePattern != null;
+    assert typePattern != null;
+    this.accessPattern = accessPattern;
+    this.namePattern = namePattern;
+    this.typePattern = typePattern;
+  }
+
+  @Override
+  public KeepFieldPattern asField() {
+    return this;
+  }
+
+  public boolean isAnyField() {
+    return accessPattern.isAny() && namePattern.isAny() && typePattern.isAny();
+  }
+
+  public KeepFieldAccessPattern getAccessPattern() {
+    return accessPattern;
+  }
+
+  public KeepFieldNamePattern getNamePattern() {
+    return namePattern;
+  }
+
+  public KeepFieldTypePattern getTypePattern() {
+    return typePattern;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    KeepFieldPattern that = (KeepFieldPattern) o;
+    return accessPattern.equals(that.accessPattern)
+        && namePattern.equals(that.namePattern)
+        && typePattern.equals(that.typePattern);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(accessPattern, namePattern, typePattern);
+  }
+
+  @Override
+  public String toString() {
+    return "KeepFieldPattern{"
+        + "access="
+        + accessPattern
+        + ", name="
+        + namePattern
+        + ", type="
+        + typePattern
+        + '}';
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldTypePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldTypePattern.java
new file mode 100644
index 0000000..beaaa55
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldTypePattern.java
@@ -0,0 +1,62 @@
+// 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.keepanno.ast;
+
+public abstract class KeepFieldTypePattern {
+
+  public static KeepFieldTypePattern any() {
+    return SomeType.ANY_TYPE_INSTANCE;
+  }
+
+  public boolean isAny() {
+    return isType() && asType().isAny();
+  }
+
+  public boolean isType() {
+    return asType() != null;
+  }
+
+  public KeepTypePattern asType() {
+    return null;
+  }
+
+  private static class SomeType extends KeepFieldTypePattern {
+
+    private static final SomeType ANY_TYPE_INSTANCE = new SomeType(KeepTypePattern.any());
+
+    private final KeepTypePattern typePattern;
+
+    private SomeType(KeepTypePattern typePattern) {
+      assert typePattern != null;
+      this.typePattern = typePattern;
+    }
+
+    @Override
+    public KeepTypePattern asType() {
+      return typePattern;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      SomeType someType = (SomeType) o;
+      return typePattern.equals(someType.typePattern);
+    }
+
+    @Override
+    public int hashCode() {
+      return typePattern.hashCode();
+    }
+
+    @Override
+    public String toString() {
+      return typePattern.toString();
+    }
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberPattern.java
index 25a57ab..b0f205e 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberPattern.java
@@ -88,4 +88,12 @@
   public KeepMethodPattern asMethod() {
     return null;
   }
+
+  public final boolean isField() {
+    return asField() != null;
+  }
+
+  public KeepFieldPattern asField() {
+    return null;
+  }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
index 7c8c032..6781a22 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
@@ -5,6 +5,9 @@
 
 import com.android.tools.r8.keepanno.ast.KeepConsequences;
 import com.android.tools.r8.keepanno.ast.KeepEdge;
+import com.android.tools.r8.keepanno.ast.KeepFieldAccessPattern;
+import com.android.tools.r8.keepanno.ast.KeepFieldNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
 import com.android.tools.r8.keepanno.ast.KeepItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepMemberPattern;
 import com.android.tools.r8.keepanno.ast.KeepMethodAccessPattern;
@@ -114,9 +117,25 @@
       printMethod(builder.append(' '), member.asMethod());
       return builder.append(" }");
     }
+    if (member.isField()) {
+      builder.append(" {");
+      printField(builder.append(' '), member.asField());
+      return builder.append(" }");
+    }
     throw new Unimplemented();
   }
 
+  private static StringBuilder printField(StringBuilder builder, KeepFieldPattern fieldPattern) {
+    if (fieldPattern.isAnyField()) {
+      return builder.append("<fields>;");
+    }
+    printAccess(builder, " ", fieldPattern.getAccessPattern());
+    printType(builder, fieldPattern.getTypePattern().asType());
+    builder.append(' ');
+    printFieldName(builder, fieldPattern.getNamePattern());
+    return builder.append(';');
+  }
+
   private static StringBuilder printMethod(StringBuilder builder, KeepMethodPattern methodPattern) {
     if (methodPattern.isAnyMethod()) {
       return builder.append("<methods>;");
@@ -143,6 +162,13 @@
         .append(')');
   }
 
+  private static StringBuilder printFieldName(
+      StringBuilder builder, KeepFieldNamePattern namePattern) {
+    return namePattern.isAny()
+        ? builder.append("*")
+        : builder.append(namePattern.asExact().getName());
+  }
+
   private static StringBuilder printMethodName(
       StringBuilder builder, KeepMethodNamePattern namePattern) {
     return namePattern.isAny()
@@ -175,6 +201,16 @@
     throw new Unimplemented();
   }
 
+  private static StringBuilder printAccess(
+      StringBuilder builder, String indent, KeepFieldAccessPattern accessPattern) {
+    if (accessPattern.isAny()) {
+      // No text will match any access pattern.
+      // Don't print the indent in this case.
+      return builder;
+    }
+    throw new Unimplemented();
+  }
+
   private static StringBuilder printClassName(
       StringBuilder builder, KeepQualifiedClassNamePattern classNamePattern) {
     if (classNamePattern.isAny()) {
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/processor/KeepEdgeProcessor.java b/src/keepanno/java/com/android/tools/r8/keepanno/processor/KeepEdgeProcessor.java
index c86857d..18d3932 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/processor/KeepEdgeProcessor.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/processor/KeepEdgeProcessor.java
@@ -16,6 +16,8 @@
 import com.android.tools.r8.keepanno.ast.KeepEdge;
 import com.android.tools.r8.keepanno.ast.KeepEdge.Builder;
 import com.android.tools.r8.keepanno.ast.KeepEdgeException;
+import com.android.tools.r8.keepanno.ast.KeepFieldNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
 import com.android.tools.r8.keepanno.ast.KeepItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepMethodNamePattern;
 import com.android.tools.r8.keepanno.ast.KeepMethodPattern;
@@ -148,14 +150,21 @@
       itemBuilder.setClassPattern(KeepQualifiedClassNamePattern.exact(typeName));
     }
     AnnotationValue methodNameValue = getAnnotationValue(mirror, Target.methodName);
+    AnnotationValue fieldNameValue = getAnnotationValue(mirror, Target.fieldName);
+    if (methodNameValue != null && fieldNameValue != null) {
+      throw new KeepEdgeException("Cannot define both a method and a field name pattern");
+    }
     if (methodNameValue != null) {
       String methodName = AnnotationStringValueVisitor.getString(methodNameValue);
       itemBuilder.setMemberPattern(
           KeepMethodPattern.builder()
               .setNamePattern(KeepMethodNamePattern.exact(methodName))
               .build());
+    } else if (fieldNameValue != null) {
+      String fieldName = AnnotationStringValueVisitor.getString(fieldNameValue);
+      itemBuilder.setMemberPattern(
+          KeepFieldPattern.builder().setNamePattern(KeepFieldNamePattern.exact(fieldName)).build());
     }
-
     builder.setItem(itemBuilder.build());
   }
 
diff --git a/src/test/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractorTest.java b/src/test/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractorTest.java
index 94001ec..4fa9b42 100644
--- a/src/test/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractorTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractorTest.java
@@ -3,20 +3,32 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.keepanno.keeprules;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.JavaCompilerTool;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.keepanno.asm.KeepEdgeReader;
 import com.android.tools.r8.keepanno.ast.KeepEdge;
+import com.android.tools.r8.keepanno.processor.KeepEdgeProcessor;
 import com.android.tools.r8.keepanno.testsource.KeepClassAndDefaultConstructorSource;
+import com.android.tools.r8.keepanno.testsource.KeepFieldSource;
 import com.android.tools.r8.keepanno.testsource.KeepSourceEdges;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.ZipUtils;
+import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
+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;
@@ -24,33 +36,77 @@
 @RunWith(Parameterized.class)
 public class KeepRuleExtractorTest extends TestBase {
 
-  private static final Class<?> SOURCE = KeepClassAndDefaultConstructorSource.class;
-  private static final String EXPECTED = KeepSourceEdges.getExpected(SOURCE);
+  private static class ParamWrapper {
+    private final Class<?> clazz;
+    private final TestParameters params;
+
+    public ParamWrapper(Class<?> clazz, TestParameters params) {
+      this.clazz = clazz;
+      this.params = params;
+    }
+
+    @Override
+    public String toString() {
+      return clazz.getSimpleName() + ", " + params.toString();
+    }
+  }
+
   private static final Path KEEP_ANNO_PATH =
       Paths.get(ToolHelper.BUILD_DIR, "classes", "java", "keepanno");
 
-  private final TestParameters parameters;
-
-  @Parameterized.Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().withAllApiLevels().build();
+  private static List<Class<?>> getTestClasses() {
+    return ImmutableList.of(KeepClassAndDefaultConstructorSource.class, KeepFieldSource.class);
   }
 
-  public KeepRuleExtractorTest(TestParameters parameters) {
-    this.parameters = parameters;
+  private final TestParameters parameters;
+  private final Class<?> source;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static List<ParamWrapper> data() {
+    TestParametersCollection params =
+        getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+    return getTestClasses().stream()
+        .flatMap(c -> params.stream().map(p -> new ParamWrapper(c, p)))
+        .collect(Collectors.toList());
+  }
+
+  public KeepRuleExtractorTest(ParamWrapper wrapper) {
+    this.parameters = wrapper.params;
+    this.source = wrapper.clazz;
   }
 
   @Test
-  public void test() throws Exception {
-    List<String> rules = getKeepRulesForClass(SOURCE);
+  public void testProcessor() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    Path out =
+        JavaCompilerTool.create(parameters.getRuntime().asCf(), temp)
+            .addAnnotationProcessors(typeName(KeepEdgeProcessor.class))
+            .addClasspathFiles(KEEP_ANNO_PATH)
+            .addClassNames(Collections.singletonList(typeName(source)))
+            .addClasspathFiles(Paths.get(ToolHelper.BUILD_DIR, "classes", "java", "test"))
+            .addClasspathFiles(ToolHelper.DEPS)
+            .compile();
+
+    String synthesizedEdgesClassName =
+        KeepEdgeProcessor.getClassTypeNameForSynthesizedEdges(source.getTypeName());
+    String entry =
+        ZipUtils.zipEntryNameForClass(Reference.classFromTypeName(synthesizedEdgesClassName));
+    byte[] bytes = ZipUtils.readSingleEntry(out, entry);
+    Set<KeepEdge> keepEdges = KeepEdgeReader.readKeepEdges(bytes);
+    assertEquals(KeepSourceEdges.getExpectedEdges(source), keepEdges);
+  }
+
+  @Test
+  public void testExtract() throws Exception {
+    List<String> rules = getKeepRulesForClass(source);
     testForR8(parameters.getBackend())
         .addClasspathFiles(KEEP_ANNO_PATH)
-        .addProgramClassesAndInnerClasses(SOURCE)
+        .addProgramClassesAndInnerClasses(source)
         .addKeepRules(rules)
-        .addKeepMainRule(SOURCE)
+        .addKeepMainRule(source)
         .setMinApi(parameters.getApiLevel())
-        .run(parameters.getRuntime(), SOURCE)
-        .assertSuccessWithOutput(EXPECTED);
+        .run(parameters.getRuntime(), source)
+        .assertSuccessWithOutput(KeepSourceEdges.getExpected(source));
   }
 
   private List<String> getKeepRulesForClass(Class<?> clazz) throws IOException {
diff --git a/src/test/java/com/android/tools/r8/keepanno/testsource/KeepFieldSource.java b/src/test/java/com/android/tools/r8/keepanno/testsource/KeepFieldSource.java
new file mode 100644
index 0000000..6f577e4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/testsource/KeepFieldSource.java
@@ -0,0 +1,35 @@
+// 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.keepanno.testsource;
+
+import com.android.tools.r8.keepanno.annotations.KeepEdge;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import java.lang.reflect.Field;
+
+@KeepEdge(
+    consequences = {
+      // Keep the reflectively accessed field.
+      @KeepTarget(classConstant = KeepFieldSource.A.class, fieldName = "f")
+    })
+public class KeepFieldSource {
+
+  public static class A {
+
+    public int f;
+
+    public A(int x) {
+      f = x;
+    }
+  }
+
+  public static void main(String[] args) throws Exception {
+    int x = 42 + args.length;
+    Object o = System.nanoTime() > 0 ? new A(x) : null;
+    Field f = o.getClass().getDeclaredField("f");
+    int y = f.getInt(o);
+    if (x == y) {
+      System.out.println("The values match!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/testsource/KeepSourceEdges.java b/src/test/java/com/android/tools/r8/keepanno/testsource/KeepSourceEdges.java
index 5637edf..7af9aee 100644
--- a/src/test/java/com/android/tools/r8/keepanno/testsource/KeepSourceEdges.java
+++ b/src/test/java/com/android/tools/r8/keepanno/testsource/KeepSourceEdges.java
@@ -5,6 +5,8 @@
 
 import com.android.tools.r8.keepanno.ast.KeepConsequences;
 import com.android.tools.r8.keepanno.ast.KeepEdge;
+import com.android.tools.r8.keepanno.ast.KeepFieldNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
 import com.android.tools.r8.keepanno.ast.KeepItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepMethodNamePattern;
 import com.android.tools.r8.keepanno.ast.KeepMethodPattern;
@@ -25,6 +27,9 @@
     if (clazz.equals(KeepClassAndDefaultConstructorSource.class)) {
       return getKeepClassAndDefaultConstructorSourceEdges();
     }
+    if (clazz.equals(KeepFieldSource.class)) {
+      return getKeepFieldSourceEdges();
+    }
     throw new RuntimeException();
   }
 
@@ -32,6 +37,9 @@
     if (clazz.equals(KeepClassAndDefaultConstructorSource.class)) {
       return getKeepClassAndDefaultConstructorSourceExpected();
     }
+    if (clazz.equals(KeepFieldSource.class)) {
+      return getKeepFieldSourceExpected();
+    }
     throw new RuntimeException();
   }
 
@@ -57,4 +65,21 @@
     KeepEdge edge = KeepEdge.builder().setConsequences(consequences).build();
     return Collections.singleton(edge);
   }
+
+  public static String getKeepFieldSourceExpected() {
+    return StringUtils.lines("The values match!");
+  }
+
+  public static Set<KeepEdge> getKeepFieldSourceEdges() {
+    Class<?> clazz = KeepFieldSource.A.class;
+    KeepQualifiedClassNamePattern name = KeepQualifiedClassNamePattern.exact(clazz.getTypeName());
+    KeepFieldPattern fieldPattern =
+        KeepFieldPattern.builder().setNamePattern(KeepFieldNamePattern.exact("f")).build();
+    KeepItemPattern fieldItem =
+        KeepItemPattern.builder().setClassPattern(name).setMemberPattern(fieldPattern).build();
+    KeepTarget fieldTarget = KeepTarget.builder().setItem(fieldItem).build();
+    KeepConsequences consequences = KeepConsequences.builder().addTarget(fieldTarget).build();
+    KeepEdge edge = KeepEdge.builder().setConsequences(consequences).build();
+    return Collections.singleton(edge);
+  }
 }