[KeepAnno] Add derived annotation @UsesReflection

Bug: b/248408342
Change-Id: Ieea7d5035d6e186da7cf0b1834f90f303f6c5804
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 ca219b2..09f7d99 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
@@ -26,6 +26,13 @@
     public static final String consequences = "consequences";
   }
 
+  public static final class UsesReflection {
+    public static final Class<com.android.tools.r8.keepanno.annotations.UsesReflection> CLASS =
+        com.android.tools.r8.keepanno.annotations.UsesReflection.class;
+    public static final String DESCRIPTOR = getDescriptor(CLASS);
+    public static final String value = "value";
+  }
+
   // Implicit hidden item which is "super type" of Condition and Target.
   public static final class Item {
     public static final String classConstant = "classConstant";
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsesReflection.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsesReflection.java
new file mode 100644
index 0000000..47ee5be
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsesReflection.java
@@ -0,0 +1,56 @@
+// 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.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to declare the reflective usages made by an item.
+ *
+ * <p>The annotation 'value' is a list of targets which are to be kept if the annotated item is
+ * kept. The structure of 'value' is identical to the 'consequences' field of a @KeepEdge
+ * annotation.
+ *
+ * <p>The translation of the @UsesReflection annotation into a @KeepEdge is as follows:
+ *
+ * <p>Assume the item of the annotation is denoted by 'CTX' which and referred to as its context.
+ *
+ * <pre>
+ * @UsesReflection(targets)
+ * ==>
+ * @KeepEdge(
+ *   consequences = targets,
+ *   preconditions = {createConditionFromContext(CTX)}
+ * )
+ *
+ * where
+ *   KeepCondition createConditionFromContext(ctx) {
+ *     if (ctx.isClass()) {
+ *       return KeepItem(classTypeName = ctx.getClassTypeName());
+ *     }
+ *     if (ctx.isMethod()) {
+ *       return KeepCondition(
+ *         classTypeName = ctx.getClassTypeName(),
+ *         methodName = ctx.getMethodName(),
+ *         methodReturnType = ctx.getMethodReturnType(),
+ *         methodParameterTypes = ctx.getMethodParameterTypes());
+ *     }
+ *     if (ctx.isField()) {
+ *       return KeepCondition(
+ *         classTypeName = ctx.getClassTypeName(),
+ *         fieldName = ctx.getFieldName()
+ *         fieldType = ctx.getFieldType());
+ *     }
+ *     // unreachable
+ *   }
+ * </pre>
+ */
+@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
+@Retention(RetentionPolicy.CLASS)
+public @interface UsesReflection {
+  KeepTarget[] value();
+}
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 dd5be82..81cea7d 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
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.keepanno.asm;
 
+import com.android.tools.r8.keepanno.annotations.KeepConstants;
 import com.android.tools.r8.keepanno.annotations.KeepConstants.Condition;
 import com.android.tools.r8.keepanno.annotations.KeepConstants.Edge;
 import com.android.tools.r8.keepanno.annotations.KeepConstants.Item;
@@ -43,66 +44,143 @@
 
   private static class KeepEdgeClassVisitor extends ClassVisitor {
     private final Parent<KeepEdge> parent;
+    private String className;
 
     KeepEdgeClassVisitor(Parent<KeepEdge> parent) {
       super(ASM_VERSION);
       this.parent = parent;
     }
 
+    private static String binaryNameToTypeName(String binaryName) {
+      return binaryName.replace('/', '.');
+    }
+
+    @Override
+    public void visit(
+        int version,
+        int access,
+        String name,
+        String signature,
+        String superName,
+        String[] interfaces) {
+      super.visit(version, access, name, signature, superName, interfaces);
+      this.className = binaryNameToTypeName(name);
+    }
+
     @Override
     public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
+      if (visible) {
+        return null;
+      }
       // Skip any visible annotations as @KeepEdge is not runtime visible.
-      if (!visible && descriptor.equals(Edge.DESCRIPTOR)) {
+      if (descriptor.equals(Edge.DESCRIPTOR)) {
         return new KeepEdgeVisitor(parent);
       }
+      if (descriptor.equals(KeepConstants.UsesReflection.DESCRIPTOR)) {
+        KeepItemPattern classItem =
+            KeepItemPattern.builder()
+                .setClassPattern(KeepQualifiedClassNamePattern.exact(className))
+                .build();
+        return new UsesReflectionVisitor(parent, classItem);
+      }
       return null;
     }
 
     @Override
     public MethodVisitor visitMethod(
         int access, String name, String descriptor, String signature, String[] exceptions) {
-      return new KeepEdgeMethodVisitor(parent);
+      return new KeepEdgeMethodVisitor(parent, className, name, descriptor);
     }
 
     @Override
     public FieldVisitor visitField(
         int access, String name, String descriptor, String signature, Object value) {
-      return new KeepEdgeFieldVisitor(parent);
+      return new KeepEdgeFieldVisitor(parent, className, name, descriptor);
     }
   }
 
   private static class KeepEdgeMethodVisitor extends MethodVisitor {
     private final Parent<KeepEdge> parent;
+    private final String className;
+    private final String methodName;
+    private final String methodDescriptor;
 
-    KeepEdgeMethodVisitor(Parent<KeepEdge> parent) {
+    KeepEdgeMethodVisitor(
+        Parent<KeepEdge> parent, String className, String methodName, String methodDescriptor) {
       super(ASM_VERSION);
       this.parent = parent;
+      this.className = className;
+      this.methodName = methodName;
+      this.methodDescriptor = methodDescriptor;
+    }
+
+    private KeepItemPattern createItemContext() {
+      Type returnType = Type.getReturnType(methodDescriptor);
+      Type[] argumentTypes = Type.getArgumentTypes(methodDescriptor);
+      // TODO(b/248408342): Defaults are "any", support setting actual return type and params.
+      return KeepItemPattern.builder()
+          .setClassPattern(KeepQualifiedClassNamePattern.exact(className))
+          .setMemberPattern(
+              KeepMethodPattern.builder()
+                  .setNamePattern(KeepMethodNamePattern.exact(methodName))
+                  .build())
+          .build();
     }
 
     @Override
     public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
       // Skip any visible annotations as @KeepEdge is not runtime visible.
-      if (!visible && descriptor.equals(Edge.DESCRIPTOR)) {
+      if (visible) {
+        return null;
+      }
+      if (descriptor.equals(Edge.DESCRIPTOR)) {
         return new KeepEdgeVisitor(parent);
       }
+      if (descriptor.equals(KeepConstants.UsesReflection.DESCRIPTOR)) {
+        return new UsesReflectionVisitor(parent, createItemContext());
+      }
       return null;
     }
   }
 
   private static class KeepEdgeFieldVisitor extends FieldVisitor {
     private final Parent<KeepEdge> parent;
+    private final String className;
+    private final String fieldName;
+    private final String fieldDescriptor;
 
-    KeepEdgeFieldVisitor(Parent<KeepEdge> parent) {
+    KeepEdgeFieldVisitor(
+        Parent<KeepEdge> parent, String className, String fieldName, String fieldDescriptor) {
       super(ASM_VERSION);
       this.parent = parent;
+      this.className = className;
+      this.fieldName = fieldName;
+      this.fieldDescriptor = fieldDescriptor;
+    }
+
+    private KeepItemPattern createItemContext() {
+      // TODO(b/248408342): Default type is "any", support setting actual field type.
+      return KeepItemPattern.builder()
+          .setClassPattern(KeepQualifiedClassNamePattern.exact(className))
+          .setMemberPattern(
+              KeepFieldPattern.builder()
+                  .setNamePattern(KeepFieldNamePattern.exact(fieldName))
+                  .build())
+          .build();
     }
 
     @Override
     public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
       // Skip any visible annotations as @KeepEdge is not runtime visible.
-      if (!visible && descriptor.equals(Edge.DESCRIPTOR)) {
+      if (visible) {
+        return null;
+      }
+      if (descriptor.equals(Edge.DESCRIPTOR)) {
         return new KeepEdgeVisitor(parent);
       }
+      if (descriptor.equals(KeepConstants.UsesReflection.DESCRIPTOR)) {
+        return new UsesReflectionVisitor(parent, createItemContext());
+      }
       return null;
     }
   }
@@ -164,6 +242,32 @@
     }
   }
 
+  private static class UsesReflectionVisitor extends AnnotationVisitorBase {
+    private final Parent<KeepEdge> parent;
+    private final KeepEdge.Builder builder = KeepEdge.builder();
+
+    UsesReflectionVisitor(Parent<KeepEdge> parent, KeepItemPattern context) {
+      this.parent = parent;
+      builder.setPreconditions(
+          KeepPreconditions.builder()
+              .addCondition(KeepCondition.builder().setItem(context).build())
+              .build());
+    }
+
+    @Override
+    public AnnotationVisitor visitArray(String name) {
+      if (name.equals(KeepConstants.UsesReflection.value)) {
+        return new KeepConsequencesVisitor(builder::setConsequences);
+      }
+      return super.visitArray(name);
+    }
+
+    @Override
+    public void visitEnd() {
+      parent.accept(builder.build());
+    }
+  }
+
   private static class KeepPreconditionsVisitor extends AnnotationVisitorBase {
     private final Parent<KeepPreconditions> parent;
     private final KeepPreconditions.Builder builder = KeepPreconditions.builder();
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepEdgeAnnotationsTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepEdgeAnnotationsTest.java
index 01ec5ac..8743b48 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepEdgeAnnotationsTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepEdgeAnnotationsTest.java
@@ -193,7 +193,7 @@
         .assertSuccessWithOutput(getExpected());
   }
 
-  private List<String> getKeepRulesForClass(Class<?> clazz) throws IOException {
+  public static List<String> getKeepRulesForClass(Class<?> clazz) throws IOException {
     Set<KeepEdge> keepEdges = KeepEdgeReader.readKeepEdges(ToolHelper.getClassAsBytes(clazz));
     List<String> rules = new ArrayList<>();
     KeepRuleExtractor extractor = new KeepRuleExtractor(rules::add);
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationTest.java
new file mode 100644
index 0000000..9bee408
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationTest.java
@@ -0,0 +1,117 @@
+// 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;
+
+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.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.UsesReflection;
+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.ArrayList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class KeepUsesReflectionAnnotationTest 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 KeepUsesReflectionAnnotationTest(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 {
+    List<String> rules = getExtractedKeepRules();
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(getInputClassesWithoutAnnotations())
+        .addKeepRules(rules)
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkOutput);
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(TestClass.class, A.class, B.class);
+  }
+
+  public List<byte[]> getInputClassesWithoutAnnotations() throws Exception {
+    List<Class<?>> classes = getInputClasses();
+    List<byte[]> transformed = new ArrayList<>(classes.size());
+    for (Class<?> clazz : classes) {
+      transformed.add(transformer(clazz).removeAllAnnotations().transform());
+    }
+    return transformed;
+  }
+
+  public List<String> getExtractedKeepRules() throws Exception {
+    List<Class<?>> classes = getInputClasses();
+    List<String> rules = new ArrayList<>();
+    for (Class<?> clazz : classes) {
+      rules.addAll(KeepEdgeAnnotationsTest.getKeepRulesForClass(clazz));
+    }
+    return rules;
+  }
+
+  private void checkOutput(CodeInspector inspector) {
+    assertThat(inspector.clazz(A.class), isPresent());
+    assertThat(inspector.clazz(B.class), isPresent());
+    assertThat(inspector.clazz(B.class).uniqueMethodWithOriginalName("bar"), isPresent());
+  }
+
+  static class A {
+
+    @UsesReflection({
+      // Ensure that the class A remains as we are assuming the contents of its name.
+      @KeepTarget(classConstant = A.class),
+      // Ensure that the class B remains as we are looking it up by reflected name.
+      @KeepTarget(classConstant = B.class),
+      // Ensure the method 'bar' remains as we are invoking it by reflected name.
+      @KeepTarget(classConstant = B.class, methodName = "bar")
+    })
+    public void foo() throws Exception {
+      Class<?> clazz = Class.forName(A.class.getTypeName().replace("$A", "$B"));
+      clazz.getDeclaredMethod("bar").invoke(clazz);
+    }
+  }
+
+  static class B {
+    public static void bar() {
+      System.out.println("Hello, world");
+    }
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) throws Exception {
+      new A().foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
index afc975a..bcd5360 100644
--- a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
+++ b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
@@ -1463,6 +1463,10 @@
         });
   }
 
+  public ClassFileTransformer removeAllAnnotations() {
+    return removeClassAnnotations().removeMethodAnnotations().removeFieldAnnotations();
+  }
+
   public ClassFileTransformer removeClassAnnotations() {
     return addClassTransformer(
         new ClassTransformer() {