[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();
+ }
+ }
+}