[KeepAnno] Add annotations UsedByReflection and UsedByNative

Bug: b/248408342
Bug: b/284104251
Change-Id: If29d482c252805ce69d6591036e9115bbcde711b
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByNative.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByNative.java
new file mode 100644
index 0000000..cd00e00
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByNative.java
@@ -0,0 +1,66 @@
+// Copyright (c) 2023, 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.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to mark a class, field or method as being accessed from native code via JNI.
+ *
+ * <p>When a class is annotated, member patterns can be used to define which members are to be kept.
+ * When no member patterns are specified the default pattern is to match just the class.
+ *
+ * <p>When a member is annotated, the member patterns cannot be used as the annotated member itself
+ * fully defines the item to be kept (i.e., itself).
+ */
+@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR})
+@Retention(RetentionPolicy.CLASS)
+public @interface UsedByNative {
+  String description() default "";
+
+  /**
+   * Conditions that should be satisfied for the annotation to be in effect.
+   *
+   * <p>Defaults to no conditions, thus trivially/unconditionally satisfied.
+   */
+  KeepCondition[] preconditions() default {};
+
+  /** Additional targets to be kept in addition to the annotated class/members. */
+  KeepTarget[] additionalTargets() default {};
+
+  /**
+   * The target kind to be kept.
+   *
+   * <p>When annotating a class without member patterns, the default kind is {@link
+   * KeepItemKind#ONLY_CLASS}.
+   *
+   * <p>When annotating a class with member patterns, the default kind is {@link
+   * KeepItemKind#CLASS_AND_MEMBERS}.
+   *
+   * <p>When annotating a member, the default kind is {@link KeepItemKind#ONLY_MEMBERS}.
+   *
+   * <p>It is not possible to use ONLY_CLASS if annotating a member.
+   */
+  KeepItemKind kind() default KeepItemKind.DEFAULT;
+
+  // Member patterns. See KeepTarget for documentation.
+  MemberAccessFlags[] memberAccess() default {};
+
+  MethodAccessFlags[] methodAccess() default {};
+
+  String methodName() default "";
+
+  String methodReturnType() default "";
+
+  String[] methodParameters() default {""};
+
+  FieldAccessFlags[] fieldAccess() default {};
+
+  String fieldName() default "";
+
+  String fieldType() default "";
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByReflection.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByReflection.java
new file mode 100644
index 0000000..3b065ca
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByReflection.java
@@ -0,0 +1,72 @@
+// Copyright (c) 2023, 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.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to mark a class, field or method as being reflectively accessed.
+ *
+ * <p>Note: Before using this annotation, consider if instead you can annotate the code that is
+ * doing reflection with {@link UsesReflection}. Annotating the reflecting code is generally more
+ * clear and maintainable, and it also naturally gives rise to edges that describe just the
+ * reflected aspects of the program. The {@link UsedByReflection} annotation is suitable for cases
+ * where the reflecting code is not under user control, or in migrating away from rules.
+ *
+ * <p>When a class is annotated, member patterns can be used to define which members are to be kept.
+ * When no member patterns are specified the default pattern is to match just the class.
+ *
+ * <p>When a member is annotated, the member patterns cannot be used as the annotated member itself
+ * fully defines the item to be kept (i.e., itself).
+ */
+@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR})
+@Retention(RetentionPolicy.CLASS)
+public @interface UsedByReflection {
+  String description() default "";
+
+  /**
+   * Conditions that should be satisfied for the annotation to be in effect.
+   *
+   * <p>Defaults to no conditions, thus trivially/unconditionally satisfied.
+   */
+  KeepCondition[] preconditions() default {};
+
+  /** Additional targets to be kept in addition to the annotated class/members. */
+  KeepTarget[] additionalTargets() default {};
+
+  /**
+   * The target kind to be kept.
+   *
+   * <p>When annotating a class without member patterns, the default kind is {@link
+   * KeepItemKind#ONLY_CLASS}.
+   *
+   * <p>When annotating a class with member patterns, the default kind is {@link
+   * KeepItemKind#CLASS_AND_MEMBERS}.
+   *
+   * <p>When annotating a member, the default kind is {@link KeepItemKind#ONLY_MEMBERS}.
+   *
+   * <p>It is not possible to use ONLY_CLASS if annotating a member.
+   */
+  KeepItemKind kind() default KeepItemKind.DEFAULT;
+
+  // Member patterns. See KeepTarget for documentation.
+  MemberAccessFlags[] memberAccess() default {};
+
+  MethodAccessFlags[] methodAccess() default {};
+
+  String methodName() default "";
+
+  String methodReturnType() default "";
+
+  String[] methodParameters() default {""};
+
+  FieldAccessFlags[] fieldAccess() default {};
+
+  String fieldName() default "";
+
+  String fieldType() 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 c8894fc..7c24ff0 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
@@ -16,6 +16,7 @@
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.MethodAccess;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Option;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Target;
+import com.android.tools.r8.keepanno.ast.AnnotationConstants.UsedByReflection;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.UsesReflection;
 import com.android.tools.r8.keepanno.ast.KeepBindings;
 import com.android.tools.r8.keepanno.ast.KeepClassReference;
@@ -53,6 +54,7 @@
 import java.util.Set;
 import java.util.function.BiPredicate;
 import java.util.function.Consumer;
+import java.util.function.Supplier;
 import org.objectweb.asm.AnnotationVisitor;
 import org.objectweb.asm.ClassReader;
 import org.objectweb.asm.ClassVisitor;
@@ -116,6 +118,10 @@
       if (descriptor.equals(AnnotationConstants.ForApi.DESCRIPTOR)) {
         return new ForApiClassVisitor(parent, this::setContext, className);
       }
+      if (descriptor.equals(AnnotationConstants.UsedByReflection.DESCRIPTOR)
+          || descriptor.equals(AnnotationConstants.UsedByNative.DESCRIPTOR)) {
+        return new UsedByReflectionClassVisitor(descriptor, parent, this::setContext, className);
+      }
       return null;
     }
 
@@ -189,6 +195,11 @@
       if (descriptor.equals(AnnotationConstants.ForApi.DESCRIPTOR)) {
         return new ForApiMemberVisitor(parent, this::setContext, createItemContext());
       }
+      if (descriptor.equals(AnnotationConstants.UsedByReflection.DESCRIPTOR)
+          || descriptor.equals(AnnotationConstants.UsedByNative.DESCRIPTOR)) {
+        return new UsedByReflectionMemberVisitor(
+            descriptor, parent, this::setContext, createItemContext());
+      }
       return null;
     }
 
@@ -246,6 +257,11 @@
       if (descriptor.equals(AnnotationConstants.ForApi.DESCRIPTOR)) {
         return new ForApiMemberVisitor(parent, this::setContext, createItemContext());
       }
+      if (descriptor.equals(AnnotationConstants.UsedByReflection.DESCRIPTOR)
+          || descriptor.equals(AnnotationConstants.UsedByNative.DESCRIPTOR)) {
+        return new UsedByReflectionMemberVisitor(
+            descriptor, parent, this::setContext, createItemContext());
+      }
       return null;
     }
   }
@@ -483,6 +499,196 @@
     }
   }
 
+  /**
+   * Parsing of @UsedByReflection or @UsedByNative on a class context.
+   *
+   * <p>When used on a class context the annotation allows the member related content of a normal
+   * item. This parser extends the base item visitor and throws an error if any class specific
+   * properties are encountered.
+   */
+  private static class UsedByReflectionClassVisitor extends KeepItemVisitorBase {
+    private final String annotationDescriptor;
+    private final String className;
+    private final Parent<KeepEdge> parent;
+    private final KeepEdge.Builder builder = KeepEdge.builder();
+    private final KeepConsequences.Builder consequences = KeepConsequences.builder();
+    private final KeepEdgeMetaInfo.Builder metaInfoBuilder = KeepEdgeMetaInfo.builder();
+
+    UsedByReflectionClassVisitor(
+        String annotationDescriptor,
+        Parent<KeepEdge> parent,
+        Consumer<KeepEdgeMetaInfo.Builder> addContext,
+        String className) {
+      this.annotationDescriptor = annotationDescriptor;
+      this.className = className;
+      this.parent = parent;
+      addContext.accept(metaInfoBuilder);
+      // The class context/holder is the annotated class.
+      visit(Item.className, className);
+    }
+
+    @Override
+    public String getAnnotationName() {
+      int sep = annotationDescriptor.lastIndexOf('/');
+      return annotationDescriptor.substring(sep + 1, annotationDescriptor.length() - 1);
+    }
+
+    @Override
+    public void visit(String name, Object value) {
+      if (name.equals(Edge.description) && value instanceof String) {
+        metaInfoBuilder.setDescription((String) value);
+        return;
+      }
+      super.visit(name, value);
+    }
+
+    @Override
+    public AnnotationVisitor visitArray(String name) {
+      if (name.equals(Edge.preconditions)) {
+        return new KeepPreconditionsVisitor(getAnnotationName(), builder::setPreconditions);
+      }
+      if (name.equals(UsedByReflection.additionalTargets)) {
+        return new KeepConsequencesVisitor(
+            getAnnotationName(),
+            additionalConsequences -> {
+              additionalConsequences.forEachTarget(consequences::addTarget);
+            });
+      }
+      return super.visitArray(name);
+    }
+
+    @Override
+    public void visitEnd() {
+      if (getKind() == null && !isDefaultMemberDeclaration()) {
+        // If no explict kind is set and member declarations have been made, keep the class too.
+        visitEnum(null, Kind.DESCRIPTOR, Kind.CLASS_AND_MEMBERS);
+      }
+      super.visitEnd();
+      KeepItemReference item = getItemReference();
+      if (item.isBindingReference()) {
+        // TODO(b/248408342): The edge can have preconditions so it should support bindings!
+        throw new KeepEdgeException("@" + getAnnotationName() + " cannot reference bindings");
+      }
+      KeepItemPattern itemPattern = item.asItemPattern();
+      String descriptor = AnnotationConstants.getDescriptorFromClassTypeName(className);
+      String itemDescriptor =
+          itemPattern.getClassReference().asClassNamePattern().getExactDescriptor();
+      if (!descriptor.equals(itemDescriptor)) {
+        throw new KeepEdgeException(
+            "@" + getAnnotationName() + " must reference its class context " + className);
+      }
+      if (itemPattern.getKind().equals(KeepItemKind.ONLY_MEMBERS)) {
+        throw new KeepEdgeException("@" + getAnnotationName() + " kind must include its class");
+      }
+      if (!itemPattern.getExtendsPattern().isAny()) {
+        throw new KeepEdgeException(
+            "@" + getAnnotationName() + " cannot define an 'extends' pattern.");
+      }
+      consequences.addTarget(KeepTarget.builder().setItemPattern(itemPattern).build());
+      parent.accept(
+          builder
+              .setMetaInfo(metaInfoBuilder.build())
+              .setConsequences(consequences.build())
+              .build());
+    }
+  }
+
+  /**
+   * Parsing of @UsedByReflection or @UsedByNative on a member context.
+   *
+   * <p>When used on a member context the annotation does not allow member related patterns.
+   */
+  private static class UsedByReflectionMemberVisitor extends AnnotationVisitorBase {
+    private final String annotationDescriptor;
+    private final Parent<KeepEdge> parent;
+    private final KeepItemPattern context;
+    private final KeepEdge.Builder builder = KeepEdge.builder();
+    private final KeepEdgeMetaInfo.Builder metaInfoBuilder = KeepEdgeMetaInfo.builder();
+
+    private final KeepConsequences.Builder consequences = KeepConsequences.builder();
+    private KeepItemKind kind = KeepItemKind.ONLY_MEMBERS;
+
+    UsedByReflectionMemberVisitor(
+        String annotationDescriptor,
+        Parent<KeepEdge> parent,
+        Consumer<KeepEdgeMetaInfo.Builder> addContext,
+        KeepItemPattern context) {
+      this.annotationDescriptor = annotationDescriptor;
+      this.parent = parent;
+      this.context = context;
+      addContext.accept(metaInfoBuilder);
+    }
+
+    @Override
+    public String getAnnotationName() {
+      int sep = annotationDescriptor.lastIndexOf('/');
+      return annotationDescriptor.substring(sep + 1, annotationDescriptor.length() - 1);
+    }
+
+    @Override
+    public void visit(String name, Object value) {
+      if (name.equals(Edge.description) && value instanceof String) {
+        metaInfoBuilder.setDescription((String) value);
+        return;
+      }
+      super.visit(name, value);
+    }
+
+    @Override
+    public void visitEnum(String name, String descriptor, String value) {
+      if (!descriptor.equals(AnnotationConstants.Kind.DESCRIPTOR)) {
+        super.visitEnum(name, descriptor, value);
+      }
+      switch (value) {
+        case Kind.DEFAULT:
+          // The default value is obtained by not assigning a kind (e.g., null in the builder).
+          break;
+        case Kind.ONLY_CLASS:
+          kind = KeepItemKind.ONLY_CLASS;
+          break;
+        case Kind.ONLY_MEMBERS:
+          kind = KeepItemKind.ONLY_MEMBERS;
+          break;
+        case Kind.CLASS_AND_MEMBERS:
+          kind = KeepItemKind.CLASS_AND_MEMBERS;
+          break;
+        default:
+          super.visitEnum(name, descriptor, value);
+      }
+    }
+
+    @Override
+    public AnnotationVisitor visitArray(String name) {
+      if (name.equals(Edge.preconditions)) {
+        return new KeepPreconditionsVisitor(getAnnotationName(), builder::setPreconditions);
+      }
+      if (name.equals(UsedByReflection.additionalTargets)) {
+        return new KeepConsequencesVisitor(
+            getAnnotationName(),
+            additionalConsequences -> {
+              additionalConsequences.forEachTarget(consequences::addTarget);
+            });
+      }
+      return super.visitArray(name);
+    }
+
+    @Override
+    public void visitEnd() {
+      if (kind.equals(KeepItemKind.ONLY_CLASS)) {
+        throw new KeepEdgeException("@" + getAnnotationName() + " kind must include its member");
+      }
+      consequences.addTarget(
+          KeepTarget.builder()
+              .setItemPattern(KeepItemPattern.builder().copyFrom(context).setKind(kind).build())
+              .build());
+      parent.accept(
+          builder
+              .setMetaInfo(metaInfoBuilder.build())
+              .setConsequences(consequences.build())
+              .build());
+    }
+  }
+
   private static class UsesReflectionVisitor extends AnnotationVisitorBase {
     private final Parent<KeepEdge> parent;
     private final KeepEdge.Builder builder = KeepEdge.builder();
@@ -766,11 +972,11 @@
   }
 
   private static class MethodDeclaration extends Declaration<KeepMethodPattern> {
-    private final String annotationName;
+    private final Supplier<String> annotationName;
     private KeepMethodAccessPattern.Builder accessBuilder = null;
     private KeepMethodPattern.Builder builder = null;
 
-    private MethodDeclaration(String annotationName) {
+    private MethodDeclaration(Supplier<String> annotationName) {
       this.annotationName = annotationName;
     }
 
@@ -845,11 +1051,11 @@
   }
 
   private static class FieldDeclaration extends Declaration<KeepFieldPattern> {
-    private final String annotationName;
+    private final Supplier<String> annotationName;
     private KeepFieldAccessPattern.Builder accessBuilder = null;
     private KeepFieldPattern.Builder builder = null;
 
-    public FieldDeclaration(String annotationName) {
+    public FieldDeclaration(Supplier<String> annotationName) {
       this.annotationName = annotationName;
     }
 
@@ -911,12 +1117,12 @@
   }
 
   private static class MemberDeclaration extends Declaration<KeepMemberPattern> {
-    private final String annotationName;
+    private final Supplier<String> annotationName;
     private KeepMemberAccessPattern.Builder accessBuilder = null;
     private final MethodDeclaration methodDeclaration;
     private final FieldDeclaration fieldDeclaration;
 
-    MemberDeclaration(String annotationName) {
+    MemberDeclaration(Supplier<String> annotationName) {
       this.annotationName = annotationName;
       methodDeclaration = new MethodDeclaration(annotationName);
       fieldDeclaration = new FieldDeclaration(annotationName);
@@ -985,7 +1191,7 @@
     private KeepItemReference itemReference = null;
 
     KeepItemVisitorBase() {
-      memberDeclaration = new MemberDeclaration(getAnnotationName());
+      memberDeclaration = new MemberDeclaration(this::getAnnotationName);
     }
 
     public KeepItemReference getItemReference() {
@@ -1124,18 +1330,18 @@
   }
 
   private static class StringArrayVisitor extends AnnotationVisitorBase {
-    private final String annotationName;
+    private final Supplier<String> annotationName;
     private final Consumer<List<String>> fn;
     private final List<String> strings = new ArrayList<>();
 
-    public StringArrayVisitor(String annotationName, Consumer<List<String>> fn) {
+    public StringArrayVisitor(Supplier<String> annotationName, Consumer<List<String>> fn) {
       this.annotationName = annotationName;
       this.fn = fn;
     }
 
     @Override
     public String getAnnotationName() {
-      return annotationName;
+      return annotationName.get();
     }
 
     @Override
@@ -1302,18 +1508,18 @@
   }
 
   private static class MemberAccessVisitor extends AnnotationVisitorBase {
-    private final String annotationName;
+    private final Supplier<String> annotationName;
     KeepMemberAccessPattern.BuilderBase<?, ?> builder;
 
     public MemberAccessVisitor(
-        String annotationName, KeepMemberAccessPattern.BuilderBase<?, ?> builder) {
+        Supplier<String> annotationName, KeepMemberAccessPattern.BuilderBase<?, ?> builder) {
       this.annotationName = annotationName;
       this.builder = builder;
     }
 
     @Override
     public String getAnnotationName() {
-      return annotationName;
+      return annotationName.get();
     }
 
     static boolean withNormalizedAccessFlag(String flag, BiPredicate<String, Boolean> fn) {
@@ -1376,7 +1582,8 @@
 
     KeepMethodAccessPattern.Builder builder;
 
-    public MethodAccessVisitor(String annotationName, KeepMethodAccessPattern.Builder builder) {
+    public MethodAccessVisitor(
+        Supplier<String> annotationName, KeepMethodAccessPattern.Builder builder) {
       super(annotationName, builder);
       this.builder = builder;
     }
@@ -1421,7 +1628,8 @@
 
     KeepFieldAccessPattern.Builder builder;
 
-    public FieldAccessVisitor(String annotationName, KeepFieldAccessPattern.Builder builder) {
+    public FieldAccessVisitor(
+        Supplier<String> annotationName, KeepFieldAccessPattern.Builder builder) {
       super(annotationName, builder);
       this.builder = builder;
     }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java
index 0daeda1..876d04d 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java
@@ -70,6 +70,23 @@
     public static final String additionalPreconditions = "additionalPreconditions";
   }
 
+  public static final class UsedByReflection {
+    public static final Class<com.android.tools.r8.keepanno.annotations.UsedByReflection> CLASS =
+        com.android.tools.r8.keepanno.annotations.UsedByReflection.class;
+    public static final String DESCRIPTOR = getDescriptor(CLASS);
+    public static final String description = "description";
+    public static final String preconditions = "preconditions";
+    public static final String additionalTargets = "additionalTargets";
+    public static final String memberAccess = "memberAccess";
+  }
+
+  public static final class UsedByNative {
+    public static final Class<com.android.tools.r8.keepanno.annotations.UsedByNative> CLASS =
+        com.android.tools.r8.keepanno.annotations.UsedByNative.class;
+    public static final String DESCRIPTOR = getDescriptor(CLASS);
+    // Content is the same as UsedByReflection.
+  }
+
   // Implicit hidden item which is "super type" of Condition and Target.
   public static final class Item {
     public static final String classFromBinding = "classFromBinding";
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepUsedByNativeAnnotationTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepUsedByNativeAnnotationTest.java
new file mode 100644
index 0000000..b3a1579
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepUsedByNativeAnnotationTest.java
@@ -0,0 +1,118 @@
+// Copyright (c) 2023, 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;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.KeepCondition;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.UsedByNative;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class KeepUsedByNativeAnnotationTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("Hello, world");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public KeepUsedByNativeAnnotationTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getInputClasses())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testWithRuleExtraction() throws Exception {
+    testForR8(parameters.getBackend())
+        .enableExperimentalKeepAnnotations()
+        .addProgramClasses(getInputClasses())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkOutput);
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(TestClass.class, A.class, B.class, C.class);
+  }
+
+  private void checkOutput(CodeInspector inspector) {
+    assertThat(inspector.clazz(A.class), isPresent());
+    assertThat(inspector.clazz(B.class), isPresent());
+    assertThat(inspector.clazz(C.class), isAbsent());
+    assertThat(inspector.clazz(A.class).method("void", "bar"), isPresent());
+    assertThat(inspector.clazz(B.class).method("void", "bar"), isPresent());
+    assertThat(inspector.clazz(B.class).method("void", "bar", "int"), isAbsent());
+  }
+
+  @UsedByNative(
+      description = "Ensure that the class A remains as we are assuming the contents of its name.",
+      preconditions = {@KeepCondition(classConstant = A.class, methodName = "foo")},
+      // The kind will default to ONLY_CLASS, so setting this to include members will keep the
+      // otherwise unused bar method.
+      kind = KeepItemKind.CLASS_AND_MEMBERS)
+  static class A {
+
+    public void foo() throws Exception {
+      Class<?> clazz = Class.forName(A.class.getTypeName().replace("$A", "$B"));
+      clazz.getDeclaredMethod("bar").invoke(clazz);
+    }
+
+    public void bar() {
+      // Unused but kept by the annotation.
+    }
+  }
+
+  static class B {
+
+    @UsedByNative(
+        // Only if A.foo is live do we need to keep this.
+        preconditions = {@KeepCondition(classConstant = A.class, methodName = "foo")},
+        // Both the class and method are reflectively accessed.
+        kind = KeepItemKind.CLASS_AND_MEMBERS)
+    public static void bar() {
+      System.out.println("Hello, world");
+    }
+
+    public static void bar(int ignore) {
+      throw new RuntimeException("UNUSED");
+    }
+  }
+
+  static class C {
+    // Unused.
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) throws Exception {
+      new A().foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepUsedByReflectionAnnotationTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepUsedByReflectionAnnotationTest.java
new file mode 100644
index 0000000..c8ea937
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepUsedByReflectionAnnotationTest.java
@@ -0,0 +1,109 @@
+// Copyright (c) 2023, 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;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.KeepCondition;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.UsedByReflection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class KeepUsedByReflectionAnnotationTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("Hello, world");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public KeepUsedByReflectionAnnotationTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getInputClasses())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testWithRuleExtraction() throws Exception {
+    testForR8(parameters.getBackend())
+        .enableExperimentalKeepAnnotations()
+        .addProgramClasses(getInputClasses())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkOutput);
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(TestClass.class, A.class, B.class, C.class);
+  }
+
+  private void checkOutput(CodeInspector inspector) {
+    assertThat(inspector.clazz(A.class), isPresent());
+    assertThat(inspector.clazz(B.class), isPresent());
+    assertThat(inspector.clazz(C.class), isAbsent());
+    assertThat(inspector.clazz(B.class).method("void", "bar"), isPresent());
+    assertThat(inspector.clazz(B.class).method("void", "bar", "int"), isAbsent());
+  }
+
+  @UsedByReflection(
+      description = "Ensure that the class A remains as we are assuming the contents of its name.")
+  static class A {
+
+    public void foo() throws Exception {
+      Class<?> clazz = Class.forName(A.class.getTypeName().replace("$A", "$B"));
+      clazz.getDeclaredMethod("bar").invoke(clazz);
+    }
+  }
+
+  static class B {
+
+    @UsedByReflection(
+        // Only if A.foo is live do we need to keep this.
+        preconditions = {@KeepCondition(classConstant = A.class, methodName = "foo")},
+        // Both the class and method are reflectively accessed.
+        kind = KeepItemKind.CLASS_AND_MEMBERS)
+    public static void bar() {
+      System.out.println("Hello, world");
+    }
+
+    public static void bar(int ignore) {
+      throw new RuntimeException("UNUSED");
+    }
+  }
+
+  static class C {
+    // Unused.
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) throws Exception {
+      new A().foo();
+    }
+  }
+}