blob: 98a2f0d2e642935eaa815e2681288a829b0b459b [file] [log] [blame]
// 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.doctests;
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.KeepConstraint;
import com.android.tools.r8.keepanno.annotations.KeepItemKind;
import com.android.tools.r8.keepanno.annotations.KeepTarget;
import com.android.tools.r8.keepanno.annotations.UsedByReflection;
import com.android.tools.r8.keepanno.annotations.UsesReflection;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.StringUtils;
import com.google.common.collect.ImmutableList;
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 UsesReflectionDocumentationTest extends TestBase {
static final String EXPECTED_METHOD_EXAMPLE = StringUtils.joinLines("on Base", "on Sub");
static final String EXPECTED_FIELD_EXAMPLE =
StringUtils.joinLines("intField = 42", "stringField = Hello!");
static final String EXPECTED =
StringUtils.lines(
EXPECTED_METHOD_EXAMPLE,
EXPECTED_FIELD_EXAMPLE,
EXPECTED_FIELD_EXAMPLE,
EXPECTED_FIELD_EXAMPLE,
EXPECTED_FIELD_EXAMPLE);
private final TestParameters parameters;
@Parameterized.Parameters(name = "{0}")
public static TestParametersCollection data() {
return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
}
public UsesReflectionDocumentationTest(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);
}
public List<Class<?>> getExampleClasses() {
return ImmutableList.of(
Example1.class, Example2.class, Example3.class, Example4.class, Example5.class);
}
static class Example1 {
static class BaseClass {
void hiddenMethod() {
System.out.println("on Base");
}
}
static class SubClass extends BaseClass {
void hiddenMethod() {
System.out.println("on Sub");
}
}
/* INCLUDE DOC: UsesReflectionOnVirtualMethod
For example, if your program is reflectively invoking a method, you
should annotate the method that is doing the reflection. The annotation must describe the
assumptions the reflective code makes.
In the following example, the method `callHiddenMethod` is looking up the method with the name
`hiddenMethod` on objects that are instances of `BaseClass`. It is then invoking the method with
no other arguments than the receiver.
The assumptions the code makes are that all methods with the name
`hiddenMethod` and the empty list of parameters must remain valid for `getDeclaredMethod` if they
are objects that are instances of the class `BaseClass` or subclasses thereof.
INCLUDE END */
static
// INCLUDE CODE: UsesReflectionOnVirtualMethod
public class MyHiddenMethodCaller {
@UsesReflection({
@KeepTarget(
instanceOfClassConstant = BaseClass.class,
methodName = "hiddenMethod",
methodParameters = {})
})
public void callHiddenMethod(BaseClass base) throws Exception {
base.getClass().getDeclaredMethod("hiddenMethod").invoke(base);
}
}
// INCLUDE END
static void run() throws Exception {
new MyHiddenMethodCaller().callHiddenMethod(new BaseClass());
new MyHiddenMethodCaller().callHiddenMethod(new SubClass());
}
}
static class Example2 {
interface PrintableFieldInterface {}
static class ClassWithFields implements PrintableFieldInterface {
final int intField = 42;
String stringField = "Hello!";
}
/* INCLUDE DOC: UsesReflectionFieldPrinter
For example, if your program is reflectively accessing the fields on a class, you should
annotate the method that is doing the reflection.
In the following example, the `printFieldValues` method takes in an object of
type `PrintableFieldInterface` and then looks for all the fields declared on the class
of the object.
The `@KeepTarget` describes these field targets. Since the printing only cares about preserving
the fields, the `@KeepTarget#kind` is set to `@KeepItemKind#ONLY_FIELDS`. Also, since printing
the field names and values only requires looking up the field, printing its name and getting
its value the `@KeepTarget#constraints` are set to just `@KeepConstraint#LOOKUP`,
`@KeepConstraint#NAME` and `@KeepConstraint#FIELD_GET`.
INCLUDE END */
static
// INCLUDE CODE: UsesReflectionFieldPrinter
public class MyFieldValuePrinter {
@UsesReflection({
@KeepTarget(
instanceOfClassConstant = PrintableFieldInterface.class,
kind = KeepItemKind.ONLY_FIELDS,
constraints = {KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.FIELD_GET})
})
public void printFieldValues(PrintableFieldInterface objectWithFields) throws Exception {
for (Field field : objectWithFields.getClass().getDeclaredFields()) {
System.out.println(field.getName() + " = " + field.get(objectWithFields));
}
}
}
// INCLUDE END
static void run() throws Exception {
new MyFieldValuePrinter().printFieldValues(new ClassWithFields());
}
}
static class Example3 {
interface PrintableFieldInterface {}
/* INCLUDE DOC: UsedByReflectionFieldPrinterOnFields
For example, the same field printing as in the above example might be part of a library.
In this example, the `MyClassWithFields` is a class you are passing to the
field-printing utility of the library. Since the library is reflectively accessing each field
we annotate them with the `@UsedByReflection` annotation.
We could additionally add the `@UsedByReflection#constraints` property as we did previously.
We elide it here for brevity.
INCLUDE END */
static
// INCLUDE CODE: UsedByReflectionFieldPrinterOnFields
public class MyClassWithFields implements PrintableFieldInterface {
@UsedByReflection final int intField = 42;
@UsedByReflection String stringField = "Hello!";
}
public static void run() throws Exception {
new FieldValuePrinterLibrary().printFieldValues(new MyClassWithFields());
}
// INCLUDE END
public static class FieldValuePrinterLibrary {
public void printFieldValues(PrintableFieldInterface objectWithFields) throws Exception {
for (Field field : objectWithFields.getClass().getDeclaredFields()) {
System.out.println(field.getName() + " = " + field.get(objectWithFields));
}
}
}
}
static class Example4 {
interface PrintableFieldInterface {}
/* INCLUDE DOC: UsedByReflectionFieldPrinterOnClass
Rather than annotate the individual fields we can annotate the holder and add a specification
similar to the `@KeepTarget`. The `@UsedByReflection#kind` specifies that only the fields are
used reflectively. In particular, the "field printer" example we are considering here does not
make reflective assumptions about the holder class, so we should not constrain it.
To be more precise let's add the `@UsedByReflection#constraints` property now. This specifies
that the fields are looked up, their names are used/assumed and their values are read.
INCLUDE END */
static
// INCLUDE CODE: UsedByReflectionFieldPrinterOnClass
@UsedByReflection(
kind = KeepItemKind.ONLY_FIELDS,
constraints = {KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.FIELD_GET})
public class MyClassWithFields implements PrintableFieldInterface {
final int intField = 42;
String stringField = "Hello!";
}
// INCLUDE END
public static void run() throws Exception {
new FieldValuePrinterLibrary().printFieldValues(new MyClassWithFields());
}
public static class FieldValuePrinterLibrary {
public void printFieldValues(PrintableFieldInterface objectWithFields) throws Exception {
for (Field field : objectWithFields.getClass().getDeclaredFields()) {
System.out.println(field.getName() + " = " + field.get(objectWithFields));
}
}
}
}
static class Example5 {
interface PrintableFieldInterface {}
/* INCLUDE DOC: UsedByReflectionFieldPrinterConditional
Our use of `@UsedByReflection` is still not as flexible as the original `@UsesReflection`. In
particular, if we change our code to no longer have any call to the library method
`printFieldValues` the shrinker will still keep all of the fields on our annotated class.
This is because the `@UsesReflection` implicitly encodes as a precondition that the annotated
method is actually used in the program. If not, the `@UsesReflection` annotation is not
"active".
Luckily we can specify the same precondition using `@UsedByReflection#preconditions`.
INCLUDE END */
static
// INCLUDE CODE: UsedByReflectionFieldPrinterConditional
@UsedByReflection(
preconditions = {
@KeepCondition(
classConstant = FieldValuePrinterLibrary.class,
methodName = "printFieldValues")
},
kind = KeepItemKind.ONLY_FIELDS,
constraints = {KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.FIELD_GET})
public class MyClassWithFields implements PrintableFieldInterface {
final int intField = 42;
String stringField = "Hello!";
}
// INCLUDE END
public static void run() throws Exception {
new FieldValuePrinterLibrary().printFieldValues(new MyClassWithFields());
}
public static class FieldValuePrinterLibrary {
public void printFieldValues(PrintableFieldInterface objectWithFields) throws Exception {
for (Field field : objectWithFields.getClass().getDeclaredFields()) {
System.out.println(field.getName() + " = " + field.get(objectWithFields));
}
}
}
}
static class TestClass {
public static void main(String[] args) throws Exception {
Example1.run();
Example2.run();
Example3.run();
Example4.run();
Example5.run();
}
}
}