[KeepAnno] Add user guide section about annotation patterns
Bug: b/319474935
Change-Id: I345dd72f5b853449fed21b53843fe3e7718a32bf
diff --git a/doc/keepanno-guide.md b/doc/keepanno-guide.md
index c41aa8b..c924bdcf 100644
--- a/doc/keepanno-guide.md
+++ b/doc/keepanno-guide.md
@@ -23,6 +23,7 @@
- [Annotating code using reflection](#using-reflection)
- [Invoking methods](#using-reflection-methods)
- [Accessing fields](#using-reflection-fields)
+ - [Accessing annotations](#using-reflection-annotations)
- [Annotating code used by reflection (or via JNI)](#used-by-reflection)
- [Annotating APIs](#apis)
- [Migrating rules to annotations](#migrating-rules)
@@ -151,6 +152,79 @@
```
+### Accessing annotations<a name="using-reflection-annotations"></a>
+
+If your program is reflectively inspecting annotations on classes, methods or fields, you
+will need to declare additional "annotation constraints" about what assumptions are made
+about the annotations.
+
+In the following example, we have defined an annotation that will record the printing name we
+would like to use for fields instead of printing the concrete field name. That may be useful
+so that the field can be renamed to follow coding conventions for example.
+
+We are only interested in matching objects that contain fields annotated by `MyNameAnnotation`,
+that is specified using [@KeepTarget.fieldAnnotatedByClassConstant](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepTarget.html#fieldAnnotatedByClassConstant()).
+
+At runtime we need to be able to find the annotation too, so we add a constraint on the
+annotation using [@KeepTarget.constrainAnnotations](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepTarget.html#constrainAnnotations()).
+
+Finally, for the sake of example, we don't actually care about the name of the fields
+themselves, so we explicitly declare the smaller set of constraints to be
+[KeepConstraint.LOOKUP](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepConstraint.html#LOOKUP) since we must find the fields via `Class.getDeclaredFields` as well as
+[KeepConstraint.VISIBILITY_RELAX](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepConstraint.html#VISIBILITY_RELAX) and [KeepConstraint.FIELD_GET](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepConstraint.html#FIELD_GET) in order to be able to get
+the actual field value without accessibility errors.
+
+The effect is that the default constraint [KeepConstraint.NAME](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepConstraint.html#NAME) is not specified which allows
+the shrinker to rename the fields at will.
+
+
+```
+public class MyAnnotationPrinter {
+
+ @Target(ElementType.FIELD)
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface MyNameAnnotation {
+ String value();
+ }
+
+ public static class MyClass {
+ @MyNameAnnotation("fieldOne")
+ public int mFieldOne = 1;
+
+ @MyNameAnnotation("fieldTwo")
+ public int mFieldTwo = 2;
+
+ public int mFieldThree = 3;
+ }
+
+ @UsesReflection(
+ @KeepTarget(
+ fieldAnnotatedByClassConstant = MyNameAnnotation.class,
+ constrainAnnotations = @AnnotationPattern(constant = MyNameAnnotation.class),
+ constraints = {
+ KeepConstraint.LOOKUP,
+ KeepConstraint.VISIBILITY_RELAX,
+ KeepConstraint.FIELD_GET
+ }))
+ public void printMyNameAnnotatedFields(Object obj) throws Exception {
+ for (Field field : obj.getClass().getDeclaredFields()) {
+ if (field.isAnnotationPresent(MyNameAnnotation.class)) {
+ System.out.println(
+ field.getAnnotation(MyNameAnnotation.class).value() + " = " + field.get(obj));
+ }
+ }
+ }
+}
+```
+
+
+If the annotations that need to be kept are not runtime
+visible annotations, then you must specify that by including the `RetentionPolicy.CLASS` value in the
+[@AnnotationPattern.retention](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/AnnotationPattern.html#retention()) property.
+An annotation is runtime visible if its definition is explicitly annotated with
+`Retention(RetentionPolicy.RUNTIME)`.
+
+
## Annotating code used by reflection (or via JNI)<a name="used-by-reflection"></a>
diff --git a/doc/keepanno-guide.template.md b/doc/keepanno-guide.template.md
index 104bf02..54d910c 100644
--- a/doc/keepanno-guide.template.md
+++ b/doc/keepanno-guide.template.md
@@ -83,6 +83,19 @@
[[[INCLUDE CODE:UsesReflectionFieldPrinter]]]
+### [Accessing annotations](using-reflection-annotations)
+
+[[[INCLUDE DOC:UsesReflectionOnAnnotations]]]
+
+[[[INCLUDE CODE:UsesReflectionOnAnnotations]]]
+
+If the annotations that need to be kept are not runtime
+visible annotations, then you must specify that by including the `RetentionPolicy.CLASS` value in the
+`@AnnotationPattern#retention` property.
+An annotation is runtime visible if its definition is explicitly annotated with
+`Retention(RetentionPolicy.RUNTIME)`.
+
+
## [Annotating code used by reflection (or via JNI)](used-by-reflection)
diff --git a/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionAnnotationsDocumentationTest.java b/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionAnnotationsDocumentationTest.java
new file mode 100644
index 0000000..a694a82
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionAnnotationsDocumentationTest.java
@@ -0,0 +1,159 @@
+// Copyright (c) 2024, 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.doctests;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+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.AnnotationPattern;
+import com.android.tools.r8.keepanno.annotations.KeepConstraint;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.UsesReflection;
+import com.android.tools.r8.keepanno.doctests.UsesReflectionAnnotationsDocumentationTest.Example1.MyAnnotationPrinter.MyClass;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.google.common.collect.ImmutableList;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.Field;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class UsesReflectionAnnotationsDocumentationTest extends TestBase {
+
+ static final String EXPECTED = StringUtils.lines("fieldOne = 1", "fieldTwo = 2");
+
+ private final TestParameters parameters;
+
+ @Parameterized.Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+ }
+
+ public UsesReflectionAnnotationsDocumentationTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void testReference() throws Exception {
+ testForRuntime(parameters)
+ .addProgramClasses(TestClass.class)
+ .addProgramClassesAndInnerClasses(getExampleClasses())
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutput(EXPECTED);
+ }
+
+ @Test
+ public void testWithRuleExtraction() throws Exception {
+ testForR8(parameters.getBackend())
+ .enableExperimentalKeepAnnotations()
+ .addProgramClasses(TestClass.class)
+ .addProgramClassesAndInnerClasses(getExampleClasses())
+ .addKeepMainRule(TestClass.class)
+ .setMinApi(parameters)
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutput(EXPECTED)
+ .inspect(
+ inspector -> {
+ ClassSubject clazz = inspector.clazz(MyClass.class);
+ assertThat(clazz.uniqueFieldWithOriginalName("mFieldOne"), isPresentAndRenamed());
+ assertThat(clazz.uniqueFieldWithOriginalName("mFieldTwo"), isPresentAndRenamed());
+ assertThat(clazz.uniqueFieldWithOriginalName("mFieldThree"), isAbsent());
+ });
+ }
+
+ public List<Class<?>> getExampleClasses() {
+ return ImmutableList.of(Example1.class);
+ }
+
+ static class Example1 {
+
+ /* INCLUDE DOC: UsesReflectionOnAnnotations
+ If your program is reflectively inspecting annotations on classes, methods or fields, you
+ will need to declare additional "annotation constraints" about what assumptions are made
+ about the annotations.
+
+ In the following example, we have defined an annotation that will record the printing name we
+ would like to use for fields instead of printing the concrete field name. That may be useful
+ so that the field can be renamed to follow coding conventions for example.
+
+ We are only interested in matching objects that contain fields annotated by `MyNameAnnotation`,
+ that is specified using `@KeepTarget#fieldAnnotatedByClassConstant`.
+
+ At runtime we need to be able to find the annotation too, so we add a constraint on the
+ annotation using `@KeepTarget#constrainAnnotations`.
+
+ Finally, for the sake of example, we don't actually care about the name of the fields
+ themselves, so we explicitly declare the smaller set of constraints to be
+ `@KeepConstraint#LOOKUP` since we must find the fields via `Class.getDeclaredFields` as well as
+ `@KeepConstraint#VISIBILITY_RELAX` and `@KeepConstraint#FIELD_GET` in order to be able to get
+ the actual field value without accessibility errors.
+
+ The effect is that the default constraint `@KeepConstraint#NAME` is not specified which allows
+ the shrinker to rename the fields at will.
+ INCLUDE END */
+
+ static
+ // INCLUDE CODE: UsesReflectionOnAnnotations
+ public class MyAnnotationPrinter {
+
+ @Target(ElementType.FIELD)
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface MyNameAnnotation {
+ String value();
+ }
+
+ public static class MyClass {
+ @MyNameAnnotation("fieldOne")
+ public int mFieldOne = 1;
+
+ @MyNameAnnotation("fieldTwo")
+ public int mFieldTwo = 2;
+
+ public int mFieldThree = 3;
+ }
+
+ @UsesReflection(
+ @KeepTarget(
+ fieldAnnotatedByClassConstant = MyNameAnnotation.class,
+ constrainAnnotations = @AnnotationPattern(constant = MyNameAnnotation.class),
+ constraints = {
+ KeepConstraint.LOOKUP,
+ KeepConstraint.VISIBILITY_RELAX,
+ KeepConstraint.FIELD_GET
+ }))
+ public void printMyNameAnnotatedFields(Object obj) throws Exception {
+ for (Field field : obj.getClass().getDeclaredFields()) {
+ if (field.isAnnotationPresent(MyNameAnnotation.class)) {
+ System.out.println(
+ field.getAnnotation(MyNameAnnotation.class).value() + " = " + field.get(obj));
+ }
+ }
+ }
+ }
+
+ // INCLUDE END
+
+ static void run() throws Exception {
+ new MyAnnotationPrinter().printMyNameAnnotatedFields(new MyAnnotationPrinter.MyClass());
+ }
+ }
+
+ static class TestClass {
+
+ public static void main(String[] args) throws Exception {
+ Example1.run();
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java b/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java
index 9376fa3..c2dec45 100644
--- a/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java
+++ b/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java
@@ -7,6 +7,7 @@
import static com.android.tools.r8.keepanno.utils.KeepItemAnnotationGenerator.quote;
import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.keepanno.annotations.AnnotationPattern;
import com.android.tools.r8.keepanno.annotations.FieldAccessFlags;
import com.android.tools.r8.keepanno.annotations.KeepBinding;
import com.android.tools.r8.keepanno.annotations.KeepCondition;
@@ -17,11 +18,14 @@
import com.android.tools.r8.keepanno.annotations.KeepTarget;
import com.android.tools.r8.keepanno.annotations.MemberAccessFlags;
import com.android.tools.r8.keepanno.annotations.MethodAccessFlags;
+import com.android.tools.r8.keepanno.annotations.StringPattern;
+import com.android.tools.r8.keepanno.annotations.TypePattern;
import com.android.tools.r8.keepanno.annotations.UsedByNative;
import com.android.tools.r8.keepanno.annotations.UsedByReflection;
import com.android.tools.r8.keepanno.annotations.UsesReflection;
import com.android.tools.r8.keepanno.doctests.ForApiDocumentationTest;
import com.android.tools.r8.keepanno.doctests.MainMethodsDocumentationTest;
+import com.android.tools.r8.keepanno.doctests.UsesReflectionAnnotationsDocumentationTest;
import com.android.tools.r8.keepanno.doctests.UsesReflectionDocumentationTest;
import com.android.tools.r8.keepanno.utils.KeepItemAnnotationGenerator.Generator;
import com.android.tools.r8.utils.FileUtils;
@@ -86,6 +90,9 @@
UsedByReflection.class,
UsedByNative.class,
KeepForApi.class,
+ StringPattern.class,
+ TypePattern.class,
+ AnnotationPattern.class,
// Enums.
KeepConstraint.class,
KeepItemKind.class,
@@ -94,6 +101,7 @@
FieldAccessFlags.class);
populateCodeAndDocReplacements(
UsesReflectionDocumentationTest.class,
+ UsesReflectionAnnotationsDocumentationTest.class,
ForApiDocumentationTest.class,
MainMethodsDocumentationTest.class);
}