[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() {