[KeepAnno] Introduce keep constraints to replace options
This CL also extends the kind structure to allow confining the item to
methods and fields.
Documentation is added for the new constraints, but follow up work is
needed to update the annotations to use the "default constraints" as
documented.
Bug: b/248408342
Change-Id: Icd5947415ad5dcca64e4043818573dd78b299d7d
diff --git a/doc/keepanno-guide.md b/doc/keepanno-guide.md
index 75d6b24..4f7665a 100644
--- a/doc/keepanno-guide.md
+++ b/doc/keepanno-guide.md
@@ -21,6 +21,8 @@
- [Introduction](#introduction)
- [Build configuration](#build-configuration)
- [Annotating code using reflection](#using-reflection)
+ - [Invoking methods](#using-reflection-methods)
+ - [Accessing fields](#using-reflection-fields)
- [Annotating code used by reflection (or via JNI)](#used-by-reflection)
- [Annotating APIs](#apis)
- [Migrating rules to annotations](#migrating-rules)
@@ -42,9 +44,10 @@
using Java annotations. The motivation for using these annotations is foremost
to place the description of what to keep closer to the program point using
reflective behavior. Doing so more directly connects the reflective code with
-the keep specification and makes it easier to maintain as the code develops. In
-addition, the annotations are defined independent from keep rules and have a
-hopefully more clear and direct meaning.
+the keep specification and makes it easier to maintain as the code develops.
+Often the keep annotations are only in effect if the annotated method is used,
+allowing more precise shrinking. In addition, the annotations are defined
+independent from keep rules and have a hopefully more clear and direct meaning.
## Build configuration<a id="build-configuration"></a>
@@ -73,16 +76,22 @@
# ... the rest of your R8 compilation command here ...
```
+
## Annotating code using reflection<a id="using-reflection"></a>
The keep annotation library defines a family of annotations depending on your
use case. You should generally prefer [@UsesReflection](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsesReflection.html) where applicable.
+Common uses of reflection are to lookup fields and methods on classes. Examples
+of such use cases are detailed below.
+
+
+### Invoking methods<a id="using-reflection-methods"></a>
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 `foo` is looking up the method with the name
+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.
@@ -92,7 +101,7 @@
```
-static class MyClass {
+public class MyHiddenMethodCaller {
@UsesReflection({
@KeepTarget(
@@ -100,7 +109,7 @@
methodName = "hiddenMethod",
methodParameters = {})
})
- public void foo(BaseClass base) throws Exception {
+ public void callHiddenMethod(BaseClass base) throws Exception {
base.getClass().getDeclaredMethod("hiddenMethod").invoke(base);
}
}
@@ -108,6 +117,41 @@
+### Accessing fields<a id="using-reflection-fields"></a>
+
+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](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepTarget.html) describes these field targets. Since the printing only cares about preserving
+the fields, the [@KeepTarget.kind](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepTarget.html#kind()) is set to [KeepItemKind.ONLY_FIELDS](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepItemKind.html#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](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepTarget.html#constraints()) are set to just [KeepConstraint.LOOKUP](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepConstraint.html#LOOKUP),
+[KeepConstraint.NAME](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepConstraint.html#NAME) 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).
+
+
+```
+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));
+ }
+ }
+}
+```
+
+
+
## Annotating code used by reflection (or via JNI)<a id="used-by-reflection"></a>
TODO
diff --git a/doc/keepanno-guide.template.md b/doc/keepanno-guide.template.md
index cfa98e2..6e3efde 100644
--- a/doc/keepanno-guide.template.md
+++ b/doc/keepanno-guide.template.md
@@ -29,9 +29,10 @@
using Java annotations. The motivation for using these annotations is foremost
to place the description of what to keep closer to the program point using
reflective behavior. Doing so more directly connects the reflective code with
-the keep specification and makes it easier to maintain as the code develops. In
-addition, the annotations are defined independent from keep rules and have a
-hopefully more clear and direct meaning.
+the keep specification and makes it easier to maintain as the code develops.
+Often the keep annotations are only in effect if the annotated method is used,
+allowing more precise shrinking. In addition, the annotations are defined
+independent from keep rules and have a hopefully more clear and direct meaning.
## [Build configuration](build-configuration)
@@ -60,16 +61,29 @@
# ... the rest of your R8 compilation command here ...
```
+
## [Annotating code using reflection](using-reflection)
The keep annotation library defines a family of annotations depending on your
use case. You should generally prefer `@UsesReflection` where applicable.
+Common uses of reflection are to lookup fields and methods on classes. Examples
+of such use cases are detailed below.
+
+
+### [Invoking methods](using-reflection-methods)
[[[INCLUDE DOC:UsesReflectionOnVirtualMethod]]]
[[[INCLUDE CODE:UsesReflectionOnVirtualMethod]]]
+### [Accessing fields](using-reflection-fields)
+
+[[[INCLUDE DOC:UsesReflectionFieldPrinter]]]
+
+[[[INCLUDE CODE:UsesReflectionFieldPrinter]]]
+
+
## [Annotating code used by reflection (or via JNI)](used-by-reflection)
TODO
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java
index e97d2b5..cbe1dac 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java
@@ -46,11 +46,21 @@
* <ul>
* <li>ONLY_CLASS
* <li>ONLY_MEMBERS
+ * <li>ONLY_METHODS
+ * <li>ONLY_FIELDS
* <li>CLASS_AND_MEMBERS
+ * <li>CLASS_AND_METHODS
+ * <li>CLASS_AND_FIELDS
* </ul>
*
- * <p>If unspecified the default for an item with no member patterns is ONLY_CLASS and if it does
- * have member patterns the default is ONLY_MEMBERS
+ * <p>If unspecified the default for an item depends on its member patterns:
+ *
+ * <ul>
+ * <li>ONLY_CLASS if no member patterns are defined
+ * <li>ONLY_METHODS if method patterns are defined
+ * <li>ONLY_FIELDS if field patterns are defined
+ * <li>ONLY_MEMBERS otherwise.
+ * </ul>
*
* @return The kind for this pattern.
*/
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstraint.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstraint.java
new file mode 100644
index 0000000..7f10457
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstraint.java
@@ -0,0 +1,210 @@
+// 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;
+
+/**
+ * Usage constraints for how a target is used and what must be kept.
+ *
+ * <p>Providing the constraints on how an item is being used allows shrinkers to optimize or remove
+ * aspects of the item that do not change that usage constraint.
+ *
+ * <p>For example, invoking a method reflectively does not use any annotations on that method, and
+ * it is safe to remove the annotations. However, it would not be safe to remove parameters from the
+ * method.
+ */
+public enum KeepConstraint {
+ /**
+ * Indicates that the target item is being looked up reflectively.
+ *
+ * <p>Looking up an item reflectively requires that it remains on its expected context, which for
+ * a method or field means it must remain on its defining class. In other words, the item cannot
+ * be removed or moved.
+ *
+ * <p>Note that looking up a member does not imply that the holder class of the member can be
+ * looked up. If both the class and the member need to be looked, make sure to have a target for
+ * both.
+ *
+ * <p>Note also that the item can be looked up within its context but no constraint is placed on
+ * its name, accessibility or any other properties of the item.
+ *
+ * <p>If assumptions are being made about other aspects, additional constraints and targets should
+ * be added to the keep annotation.
+ */
+ LOOKUP,
+
+ /**
+ * Indicates that the name of the target item is being used.
+ *
+ * <p>This usage constraint is needed if the target is being looked up reflectively by using its
+ * name. Setting it will prohibit renaming of the target item.
+ *
+ * <p>Note that preserving the name of a member does not imply that the holder class of the member
+ * will preserve its qualified or simple name. If both the class and the member need to preserve
+ * their names, make sure to have a target for both.
+ *
+ * <p>Note that preserving the name of a member does not preserve the types of its parameters or
+ * its return type for methods or the type for fields.
+ */
+ NAME,
+
+ /**
+ * Indicates that the visibility of the target must be at least as visible as declared.
+ *
+ * <p>Setting this constraint ensures that any (reflective) access to the target that is allowed
+ * remains valid. In other words, a public class, field or method must remain public. For a
+ * non-public target its visibility may be relaxed in the direction: {@code private ->
+ * package-private -> protected -> public}.
+ *
+ * <p>Note that this constraint does not place any restrictions on any other accesses flags than
+ * visibility. In particular, flags such a static, final and abstract may change.
+ *
+ * <p>Used together with {@link #VISIBILITY_RESTRICT} the visibility will remain invariant.
+ */
+ VISIBILITY_RELAX,
+
+ /**
+ * Indicates that the visibility of the target must be at most as visible as declared.
+ *
+ * <p>Setting this constraint ensures that any (reflective) access to the target that would fail
+ * will continue to fail. In other words, a private class, field or method must remain private.
+ * Concretely the visibility of the target item may be restricted in the direction: {@code public
+ * -> protected -> package-private -> private}.
+ *
+ * <p>Note that this constraint does not place any restrictions on any other accesses flags than
+ * visibility. In particular, flags such a static, final and abstract may change.
+ *
+ * <p>Used together with {@link #VISIBILITY_RELAX} the visibility will remain invariant.
+ */
+ VISIBILITY_RESTRICT,
+
+ /**
+ * Indicates that the class target is being instantiated reflectively.
+ *
+ * <p>This usage constraint is only valid on class targets.
+ *
+ * <p>Being instantiated prohibits reasoning about the class instances at compile time. In other
+ * words, the compiler must assume the class to be possibly instantiated at all times.
+ *
+ * <p>Note that the constraint {@link KeepConstraint#LOOKUP} is needed to reflectively obtain a
+ * class. This constraint only implies that if the class is referenced in the program it may be
+ * instantiated.
+ */
+ CLASS_INSTANTIATE,
+
+ /**
+ * Indicates that the method target is being invoked reflectively.
+ *
+ * <p>This usage constraint is only valid on method targets.
+ *
+ * <p>To be invoked reflectively the method must retain the structure of the method. Thus, unused
+ * arguments cannot be removed. However, it does not imply preserving the types of its parameters
+ * or its return type. If the parameter types are being obtained reflectively then those need a
+ * keep target independent of the method.
+ *
+ * <p>Note that the constraint {@link KeepConstraint#LOOKUP} is needed to reflectively obtain a
+ * method reference. This constraint only implies that if the method is referenced in the program
+ * it may be reflectively invoked.
+ */
+ METHOD_INVOKE,
+
+ /**
+ * Indicates that the field target is reflectively read from.
+ *
+ * <p>This usage constraint is only valid on field targets.
+ *
+ * <p>A field that has its value read from, requires that the field value must remain. Thus, if
+ * field remains, its value cannot be replaced. It can still be removed in full, be inlined and
+ * its value reasoned about at compile time.
+ *
+ * <p>Note that the constraint {@link KeepConstraint#LOOKUP} is needed to reflectively obtain
+ * access to the field. This constraint only implies that if a field is referenced then it may be
+ * reflectively read.
+ */
+ FIELD_GET,
+
+ /**
+ * Indicates that the field target is reflectively written to.
+ *
+ * <p>This usage constraint is only valid on field targets.
+ *
+ * <p>A field that has its value written to, requires that the field value must be treated as
+ * unknown.
+ *
+ * <p>Note that the constraint {@link KeepConstraint#LOOKUP} is needed to reflectively obtain
+ * access to the field. This constraint only implies that if a field is referenced then it may be
+ * reflectively written.
+ */
+ FIELD_SET,
+
+ /**
+ * Indicates that the target method can be replaced by an alternative definition at runtime.
+ *
+ * <p>This usage constraint is only valid on method targets.
+ *
+ * <p>Replacing a method implies that the concrete implementation of the target cannot be known at
+ * compile time. Thus, it cannot be moved or otherwise assumed to have particular properties.
+ * Being able to replace the method still allows the compiler to fully remove it if it is
+ * statically found to be unused.
+ *
+ * <p>Note also that no restriction is placed on the method name. To ensure the same name add
+ * {@link #NAME} to the constraint set.
+ */
+ METHOD_REPLACE,
+
+ /**
+ * Indicates that the target field can be replaced by an alternative definition at runtime.
+ *
+ * <p>This usage constraint is only valid on field targets.
+ *
+ * <p>Replacing a field implies that the concrete implementation of the target cannot be known at
+ * compile time. Thus, it cannot be moved or otherwise assumed to have particular properties.
+ * Being able to replace the method still allows the compiler to fully remove it if it is
+ * statically found to be unused.
+ *
+ * <p>Note also that no restriction is placed on the field name. To ensure the same name add
+ * {@link #NAME} to the constraint set.
+ */
+ FIELD_REPLACE,
+
+ /**
+ * Indicates that the target item must never be inlined or merged.
+ *
+ * <p>This ensures that if the item is actually used in the program it will remain in some form.
+ * For example, a method may still be renamed, but it will be present as a frame in stack traces
+ * produced by the runtime (before potentially being retraced). For classes, they too may be
+ * renamed, but will not have been merged with other classes or have their allocations fully
+ * eliminated (aka class inlining).
+ *
+ * <p>For members this also ensures that the field value or method body cannot be reasoned about
+ * outside the item itself. For example, a field value cannot be assumed to be a particular value,
+ * and a method cannot be assumed to have particular properties for callers, such as always
+ * throwing or a constant return value.
+ */
+ NEVER_INLINE,
+
+ /**
+ * Indicates that the class hierarchy below the target class may be extended at runtime.
+ *
+ * <p>This ensures that new subtypes of the target class can later be linked and/or class loaded
+ * at runtime.
+ *
+ * <p>This does not ensure that the class remains if it is otherwise dead code and can be fully
+ * removed.
+ *
+ * <p>Note that this constraint does not ensure that particular methods remain on the target
+ * class. If methods or fields of the target class are being targeted by a subclass that was
+ * classloaded or linked later, then keep annotations are needed for those targets too. Such
+ * non-visible uses requires the same annotations to preserve as for reflective uses.
+ */
+ CLASS_OPEN_HIERARCHY,
+
+ /**
+ * Indicates that the annotations on the target item are being accessed reflectively.
+ *
+ * <p>If only a particular set of annotations is accessed, you should set the TBD property on the
+ * target item.
+ */
+ ANNOTATIONS,
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepItemKind.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepItemKind.java
index 7530bf1..cb08d19 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepItemKind.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepItemKind.java
@@ -6,6 +6,10 @@
public enum KeepItemKind {
ONLY_CLASS,
ONLY_MEMBERS,
+ ONLY_METHODS,
+ ONLY_FIELDS,
CLASS_AND_MEMBERS,
+ CLASS_AND_METHODS,
+ CLASS_AND_FIELDS,
DEFAULT
}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
index 8ad5098..7052035 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
@@ -36,40 +36,93 @@
* <ul>
* <li>ONLY_CLASS
* <li>ONLY_MEMBERS
+ * <li>ONLY_METHODS
+ * <li>ONLY_FIELDS
* <li>CLASS_AND_MEMBERS
+ * <li>CLASS_AND_METHODS
+ * <li>CLASS_AND_FIELDS
* </ul>
*
- * <p>If unspecified the default for an item with no member patterns is ONLY_CLASS and if it does
- * have member patterns the default is ONLY_MEMBERS
+ * <p>If unspecified the default for an item depends on its member patterns:
+ *
+ * <ul>
+ * <li>ONLY_CLASS if no member patterns are defined
+ * <li>ONLY_METHODS if method patterns are defined
+ * <li>ONLY_FIELDS if field patterns are defined
+ * <li>ONLY_MEMBERS otherwise.
+ * </ul>
*
* @return The kind for this pattern.
*/
KeepItemKind kind() default KeepItemKind.DEFAULT;
/**
- * Define the options that are allowed to be modified.
+ * Define the usage constraints of the target.
*
- * <p>The specified options do not need to be preserved for the target.
+ * <p>The specified constraints must remain valid for the target.
*
- * <p>Mutually exclusive with the property `disallow` also defining options.
+ * <p>The default constraints depend on the type of the target.
*
- * <p>If nothing is specified for options the default is "allow none" / "disallow all".
+ * <ul>
+ * <li>For classes, the default is {{@link KeepConstraint#LOOKUP}, {@link KeepConstraint#NAME},
+ * {@link KeepConstraint#CLASS_INSTANTIATE}}
+ * <li>For methods, the default is {{@link KeepConstraint#LOOKUP}, {@link KeepConstraint#NAME},
+ * {@link KeepConstraint#METHOD_INVOKE}}
+ * <li>For fields, the default is {{@link KeepConstraint#LOOKUP}, {@link KeepConstraint#NAME},
+ * {@link KeepConstraint#FIELD_GET}, {@link KeepConstraint#FIELD_SET}}
+ * </ul>
*
- * @return Options allowed to be modified for the target.
+ * <p>Mutually exclusive with the following other properties defining constraints:
+ *
+ * <ul>
+ * <li>allow
+ * <li>disallow
+ * </ul>
+ *
+ * <p>If nothing is specified for constraints the default is the default for {@link #constraints}.
+ *
+ * @return Usage constraints for the target.
*/
+ KeepConstraint[] constraints() default {};
+
+ /**
+ * Define the constraints that are allowed to be modified.
+ *
+ * <p>The specified option constraints do not need to be preserved for the target.
+ *
+ * <p>Mutually exclusive with the following other properties defining constraints:
+ *
+ * <ul>
+ * <li>constraints
+ * <li>disallow
+ * </ul>
+ *
+ * <p>If nothing is specified for constraints the default is the default for {@link #constraints}.
+ *
+ * @return Option constraints allowed to be modified for the target.
+ * @deprecated Use {@link #constraints} instead.
+ */
+ @Deprecated
KeepOption[] allow() default {};
/**
- * Define the options that are not allowed to be modified.
+ * Define the constraints that are not allowed to be modified.
*
- * <p>The specified options *must* be preserved for the target.
+ * <p>The specified option constraints *must* be preserved for the target.
*
- * <p>Mutually exclusive with the property `allow` also defining options.
+ * <p>Mutually exclusive with the following other properties defining constraints:
*
- * <p>If nothing is specified for options the default is "allow none" / "disallow all".
+ * <ul>
+ * <li>constraints
+ * <li>allow
+ * </ul>
*
- * @return Options not allowed to be modified for the target.
+ * <p>If nothing is specified for constraints the default is the default for {@link #constraints}.
+ *
+ * @return Option constraints not allowed to be modified for the target.
+ * @deprecated Use {@link #constraints} instead.
*/
+ @Deprecated
KeepOption[] disallow() 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 bc466c5..d8dd158 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
@@ -7,6 +7,7 @@
import com.android.tools.r8.keepanno.ast.AnnotationConstants;
import com.android.tools.r8.keepanno.ast.AnnotationConstants.Binding;
import com.android.tools.r8.keepanno.ast.AnnotationConstants.Condition;
+import com.android.tools.r8.keepanno.ast.AnnotationConstants.Constraints;
import com.android.tools.r8.keepanno.ast.AnnotationConstants.Edge;
import com.android.tools.r8.keepanno.ast.AnnotationConstants.FieldAccess;
import com.android.tools.r8.keepanno.ast.AnnotationConstants.ForApi;
@@ -88,6 +89,84 @@
KeepQualifiedClassNamePattern.exact(className));
}
+ /** Internal copy of the user-facing KeepItemKind */
+ public enum ItemKind {
+ ONLY_CLASS,
+ ONLY_MEMBERS,
+ ONLY_METHODS,
+ ONLY_FIELDS,
+ CLASS_AND_MEMBERS,
+ CLASS_AND_METHODS,
+ CLASS_AND_FIELDS;
+
+ private static ItemKind fromString(String name) {
+ switch (name) {
+ case Kind.ONLY_CLASS:
+ return ONLY_CLASS;
+ case Kind.ONLY_MEMBERS:
+ return ONLY_MEMBERS;
+ case Kind.ONLY_METHODS:
+ return ONLY_METHODS;
+ case Kind.ONLY_FIELDS:
+ return ONLY_FIELDS;
+ case Kind.CLASS_AND_MEMBERS:
+ return CLASS_AND_MEMBERS;
+ case Kind.CLASS_AND_METHODS:
+ return CLASS_AND_METHODS;
+ case Kind.CLASS_AND_FIELDS:
+ return CLASS_AND_FIELDS;
+ default:
+ return null;
+ }
+ }
+
+ private boolean isOnlyClass() {
+ return equals(ONLY_CLASS);
+ }
+
+ private boolean requiresMembers() {
+ // If requiring members it is fine to have the more specific methods or fields.
+ return includesMembers();
+ }
+
+ private boolean requiresMethods() {
+ return equals(ONLY_METHODS) || equals(CLASS_AND_METHODS);
+ }
+
+ private boolean requiresFields() {
+ return equals(ONLY_FIELDS) || equals(CLASS_AND_FIELDS);
+ }
+
+ private boolean includesClassAndMembers() {
+ return includesClass() && includesMembers();
+ }
+
+ private boolean includesClass() {
+ return equals(ONLY_CLASS)
+ || equals(CLASS_AND_MEMBERS)
+ || equals(CLASS_AND_METHODS)
+ || equals(CLASS_AND_FIELDS);
+ }
+
+ private boolean includesMembers() {
+ return !equals(ONLY_CLASS);
+ }
+
+ private boolean includesMethod() {
+ return equals(ONLY_MEMBERS)
+ || equals(ONLY_METHODS)
+ || equals(CLASS_AND_MEMBERS)
+ || equals(CLASS_AND_METHODS);
+ }
+
+ private boolean includesField() {
+ return equals(ONLY_MEMBERS)
+ || equals(ONLY_FIELDS)
+ || equals(CLASS_AND_MEMBERS)
+ || equals(CLASS_AND_FIELDS);
+ }
+ }
+
private static class KeepEdgeClassVisitor extends ClassVisitor {
private final Parent<KeepDeclaration> parent;
private String className;
@@ -479,7 +558,7 @@
@Override
public void visitEnd() {
- if (!getKind().equals(Kind.ONLY_CLASS) && isDefaultMemberDeclaration()) {
+ if (!getKind().isOnlyClass() && isDefaultMemberDeclaration()) {
// If no member declarations have been made, set public & protected as the default.
AnnotationVisitor v = visitArray(Item.memberAccess);
v.visitEnum(null, MemberAccess.DESCRIPTOR, MemberAccess.PUBLIC);
@@ -711,7 +790,7 @@
private final KeepEdgeMetaInfo.Builder metaInfoBuilder = KeepEdgeMetaInfo.builder();
private final UserBindingsHelper bindingsHelper = new UserBindingsHelper();
private final KeepConsequences.Builder consequences = KeepConsequences.builder();
- private String kind = Kind.ONLY_MEMBERS;
+ private ItemKind kind = KeepEdgeReader.ItemKind.ONLY_MEMBERS;
UsedByReflectionMemberVisitor(
String annotationDescriptor,
@@ -744,14 +823,11 @@
if (!descriptor.equals(AnnotationConstants.Kind.DESCRIPTOR)) {
super.visitEnum(name, descriptor, value);
}
- switch (value) {
- case Kind.ONLY_CLASS:
- case Kind.ONLY_MEMBERS:
- case Kind.CLASS_AND_MEMBERS:
- kind = value;
- break;
- default:
- super.visitEnum(name, descriptor, value);
+ KeepEdgeReader.ItemKind kind = KeepEdgeReader.ItemKind.fromString(value);
+ if (kind != null) {
+ this.kind = kind;
+ } else {
+ super.visitEnum(name, descriptor, value);
}
}
@@ -774,15 +850,16 @@
@Override
public void visitEnd() {
- if (Kind.ONLY_CLASS.equals(kind)) {
+ if (kind.isOnlyClass()) {
throw new KeepEdgeException("@" + getAnnotationName() + " kind must include its member");
}
assert context.isMemberItemPattern();
KeepMemberItemPattern memberContext = context.asMemberItemPattern();
- if (Kind.CLASS_AND_MEMBERS.equals(kind)) {
+ if (kind.includesClass()) {
consequences.addTarget(
KeepTarget.builder().setItemReference(memberContext.getClassReference()).build());
}
+ validateConsistentKind(memberContext.getMemberPattern());
consequences.addTarget(KeepTarget.builder().setItemPattern(context).build());
parent.accept(
builder
@@ -791,6 +868,18 @@
.setConsequences(consequences.build())
.build());
}
+
+ private void validateConsistentKind(KeepMemberPattern memberPattern) {
+ if (memberPattern.isGeneralMember()) {
+ throw new KeepEdgeException("Unexpected general pattern for context.");
+ }
+ if (memberPattern.isMethod() && !kind.includesMethod()) {
+ throw new KeepEdgeException("Kind " + kind + " cannot be use when annotating a method");
+ }
+ if (memberPattern.isField() && !kind.includesField()) {
+ throw new KeepEdgeException("Kind " + kind + " cannot be use when annotating a field");
+ }
+ }
}
private static class UsesReflectionVisitor extends AnnotationVisitorBase {
@@ -1488,7 +1577,7 @@
private abstract static class KeepItemVisitorBase extends AnnotationVisitorBase {
private String memberBindingReference = null;
- private String kind = null;
+ private ItemKind kind = null;
private final ClassDeclaration classDeclaration = new ClassDeclaration(this::getBindingsHelper);
private final MemberDeclaration memberDeclaration;
@@ -1505,8 +1594,14 @@
if (itemReference == null) {
throw new KeepEdgeException("Item reference not finalized. Missing call to visitEnd()");
}
- if (Kind.CLASS_AND_MEMBERS.equals(kind)) {
- // If kind is set then visitEnd ensures that this cannot be a binding reference.
+ if (itemReference.isBindingReference()) {
+ return Collections.singletonList(itemReference);
+ }
+ // Kind is only null if item is a "binding reference".
+ if (kind == null) {
+ throw new KeepEdgeException("Unexpected state: unknown kind for an item pattern");
+ }
+ if (kind.includesClassAndMembers()) {
assert !itemReference.isBindingReference();
KeepItemPattern itemPattern = itemReference.asItemPattern();
KeepClassItemReference classReference;
@@ -1536,8 +1631,10 @@
return Collections.singletonList(itemReference);
}
// Kind is only null if item is a "binding reference".
- assert kind != null;
- if (Kind.CLASS_AND_MEMBERS.equals(kind)) {
+ if (kind == null) {
+ throw new KeepEdgeException("Unexpected state: unknown kind for an item pattern");
+ }
+ if (kind.includesClassAndMembers()) {
KeepItemPattern itemPattern = itemReference.asItemPattern();
// Ensure we have a member item linked to the correct class.
KeepMemberItemPattern memberItemPattern;
@@ -1577,7 +1674,7 @@
return itemReference;
}
- public String getKind() {
+ public ItemKind getKind() {
return kind;
}
@@ -1590,14 +1687,11 @@
if (!descriptor.equals(AnnotationConstants.Kind.DESCRIPTOR)) {
super.visitEnum(name, descriptor, value);
}
- switch (value) {
- case Kind.ONLY_CLASS:
- case Kind.ONLY_MEMBERS:
- case Kind.CLASS_AND_MEMBERS:
- kind = value;
- break;
- default:
- super.visitEnum(name, descriptor, value);
+ ItemKind kind = ItemKind.fromString(value);
+ if (kind != null) {
+ this.kind = kind;
+ } else {
+ super.visitEnum(name, descriptor, value);
}
}
@@ -1636,20 +1730,73 @@
itemReference = KeepBindingReference.forMember(symbol).toItemReference();
} else {
KeepMemberPattern memberPattern = memberDeclaration.getValue();
- // If the kind is not set (default) then the content of the members determines the kind.
+ // If no explicit kind is set, extract it based on the member pattern.
if (kind == null) {
- kind = memberPattern.isNone() ? Kind.ONLY_CLASS : Kind.ONLY_MEMBERS;
+ if (memberPattern.isMethod()) {
+ kind = ItemKind.ONLY_METHODS;
+ } else if (memberPattern.isField()) {
+ kind = ItemKind.ONLY_FIELDS;
+ } else if (memberPattern.isGeneralMember()) {
+ kind = ItemKind.ONLY_MEMBERS;
+ } else {
+ assert memberPattern.isNone();
+ kind = ItemKind.ONLY_CLASS;
+ }
+ }
+
+ if (kind.isOnlyClass() && !memberPattern.isNone()) {
+ throw new KeepEdgeException("Item pattern for members is incompatible with kind " + kind);
+ }
+
+ // Refine the member pattern to be as precise as the specified kind.
+ if (kind.requiresMethods() && !memberPattern.isMethod()) {
+ if (memberPattern.isGeneralMember()) {
+ memberPattern =
+ KeepMethodPattern.builder()
+ .setAccessPattern(
+ KeepMethodAccessPattern.builder()
+ .copyOfMemberAccess(memberPattern.getAccessPattern())
+ .build())
+ .build();
+ } else if (memberPattern.isNone()) {
+ memberPattern = KeepMethodPattern.allMethods();
+ } else {
+ assert memberPattern.isField();
+ throw new KeepEdgeException(
+ "Item pattern for fields is incompatible with kind " + kind);
+ }
+ }
+
+ if (kind.requiresFields() && !memberPattern.isField()) {
+ if (memberPattern.isGeneralMember()) {
+ memberPattern =
+ KeepFieldPattern.builder()
+ .setAccessPattern(
+ KeepFieldAccessPattern.builder()
+ .copyOfMemberAccess(memberPattern.getAccessPattern())
+ .build())
+ .build();
+ } else if (memberPattern.isNone()) {
+ memberPattern = KeepFieldPattern.allFields();
+ } else {
+ assert memberPattern.isMethod();
+ throw new KeepEdgeException(
+ "Item pattern for methods is incompatible with kind " + kind);
+ }
+ }
+
+ if (kind.requiresMembers() && memberPattern.isNone()) {
+ memberPattern = KeepMemberPattern.allMembers();
}
KeepClassItemReference classReference = classDeclaration.getValue();
- if (kind.equals(Kind.ONLY_CLASS)) {
+ if (kind.isOnlyClass()) {
itemReference = classReference;
} else {
KeepItemPattern itemPattern =
KeepMemberItemPattern.builder()
.setClassReference(classReference)
- .setMemberPattern(
- memberPattern.isNone() ? KeepMemberPattern.allMembers() : memberPattern)
+ .setMemberPattern(memberPattern)
.build();
itemReference = itemPattern.toItemReference();
}
@@ -1759,6 +1906,11 @@
@Override
AnnotationVisitor parseArray(String name, Consumer<KeepOptions> setValue) {
+ if (name.equals(AnnotationConstants.Target.constraints)) {
+ return new KeepConstraintsVisitor(
+ annotationName,
+ options -> setValue.accept(KeepOptions.disallowBuilder().addAll(options).build()));
+ }
if (name.equals(AnnotationConstants.Target.disallow)) {
return new KeepOptionsVisitor(
annotationName,
@@ -1845,6 +1997,74 @@
}
}
+ private static class KeepConstraintsVisitor extends AnnotationVisitorBase {
+
+ private final String annotationName;
+ private final Parent<Collection<KeepOption>> parent;
+ private final Set<KeepOption> options = new HashSet<>();
+
+ public KeepConstraintsVisitor(String annotationName, Parent<Collection<KeepOption>> parent) {
+ this.annotationName = annotationName;
+ this.parent = parent;
+ }
+
+ @Override
+ public String getAnnotationName() {
+ return annotationName;
+ }
+
+ @Override
+ public void visitEnum(String ignore, String descriptor, String value) {
+ if (!descriptor.equals(AnnotationConstants.Constraints.DESCRIPTOR)) {
+ super.visitEnum(ignore, descriptor, value);
+ }
+ switch (value) {
+ case Constraints.LOOKUP:
+ options.add(KeepOption.SHRINKING);
+ break;
+ case Constraints.NAME:
+ options.add(KeepOption.OBFUSCATING);
+ break;
+ case Constraints.VISIBILITY_RELAX:
+ // The compiler currently satisfies that access is never restricted.
+ break;
+ case Constraints.VISIBILITY_RESTRICT:
+ // We don't have directional rules so this prohibits any modification.
+ options.add(KeepOption.ACCESS_MODIFICATION);
+ break;
+ case Constraints.CLASS_INSTANTIATE:
+ case Constraints.METHOD_INVOKE:
+ case Constraints.FIELD_GET:
+ case Constraints.FIELD_SET:
+ // These options are the item-specific actual uses of the items.
+ // Allocating, invoking and read/writing all imply that the item cannot be "optimized"
+ // at compile time. It would be natural to refine the field specific uses but that is
+ // not expressible as keep options.
+ options.add(KeepOption.OPTIMIZING);
+ break;
+ case Constraints.METHOD_REPLACE:
+ case Constraints.FIELD_REPLACE:
+ case Constraints.NEVER_INLINE:
+ case Constraints.CLASS_OPEN_HIERARCHY:
+ options.add(KeepOption.OPTIMIZING);
+ break;
+ case Constraints.ANNOTATIONS:
+ // The annotation constrain only implies that annotations should remain, no restrictions
+ // are on the item otherwise.
+ options.add(KeepOption.ANNOTATION_REMOVAL);
+ break;
+ default:
+ super.visitEnum(ignore, descriptor, value);
+ }
+ }
+
+ @Override
+ public void visitEnd() {
+ parent.accept(options);
+ super.visitEnd();
+ }
+ }
+
private static class KeepOptionsVisitor extends AnnotationVisitorBase {
private final String annotationName;
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 7ca4306..6ae281b 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
@@ -111,6 +111,7 @@
public static final String DESCRIPTOR =
"Lcom/android/tools/r8/keepanno/annotations/KeepTarget;";
public static final String kind = "kind";
+ public static final String constraints = "constraints";
public static final String allow = "allow";
public static final String disallow = "disallow";
}
@@ -121,7 +122,30 @@
"Lcom/android/tools/r8/keepanno/annotations/KeepItemKind;";
public static final String ONLY_CLASS = "ONLY_CLASS";
public static final String ONLY_MEMBERS = "ONLY_MEMBERS";
+ public static final String ONLY_METHODS = "ONLY_METHODS";
+ public static final String ONLY_FIELDS = "ONLY_FIELDS";
public static final String CLASS_AND_MEMBERS = "CLASS_AND_MEMBERS";
+ public static final String CLASS_AND_METHODS = "CLASS_AND_METHODS";
+ public static final String CLASS_AND_FIELDS = "CLASS_AND_FIELDS";
+ }
+
+ public static final class Constraints {
+ public static final String SIMPLE_NAME = "KeepConstraint";
+ public static final String DESCRIPTOR =
+ "Lcom/android/tools/r8/keepanno/annotations/KeepConstraint;";
+ public static final String LOOKUP = "LOOKUP";
+ public static final String NAME = "NAME";
+ public static final String VISIBILITY_RELAX = "VISIBILITY_RELAX";
+ public static final String VISIBILITY_RESTRICT = "VISIBILITY_RESTRICT";
+ public static final String CLASS_INSTANTIATE = "CLASS_INSTANTIATE";
+ public static final String METHOD_INVOKE = "METHOD_INVOKE";
+ public static final String FIELD_GET = "FIELD_GET";
+ public static final String FIELD_SET = "FIELD_SET";
+ public static final String METHOD_REPLACE = "METHOD_REPLACE";
+ public static final String FIELD_REPLACE = "FIELD_REPLACE";
+ public static final String NEVER_INLINE = "NEVER_INLINE";
+ public static final String CLASS_OPEN_HIERARCHY = "CLASS_OPEN_HIERARCHY";
+ public static final String ANNOTATIONS = "ANNOTATIONS";
}
public static final class Option {
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepOptions.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepOptions.java
index ba647eb..284a0ac 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepOptions.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepOptions.java
@@ -23,7 +23,7 @@
OPTIMIZING,
OBFUSCATING,
ACCESS_MODIFICATION,
- ANNOTATION_REMOVAL,
+ ANNOTATION_REMOVAL
}
public static KeepOptions keepAll() {
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java
index 3669002..5d5c357 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java
@@ -81,6 +81,10 @@
public static void printKeepOptions(StringBuilder builder, KeepOptions options) {
for (KeepOption option : KeepOption.values()) {
+ if (option == KeepOption.ANNOTATION_REMOVAL) {
+ // Annotation removal is a testing option, we can't reliably extract it out into rules.
+ continue;
+ }
if (options.isAllowed(option)) {
builder.append(",allow").append(getOptionString(option));
}
diff --git a/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java b/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java
index 8baa5ce..f0ea424 100644
--- a/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java
@@ -77,8 +77,7 @@
.build())
.build();
// Disallow will issue the full inverse of the known options, e.g., 'allowaccessmodification'.
- List<String> options =
- ImmutableList.of("shrinking", "obfuscation", "accessmodification", "annotationremoval");
+ List<String> options = ImmutableList.of("shrinking", "obfuscation", "accessmodification");
String allows = String.join(",allow", options);
// The "any" item will be split in two rules, one for the targeted types and one for the
// targeted members.
diff --git a/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionDocumentationTest.java b/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionDocumentationTest.java
index 1225db2..0cf7639 100644
--- a/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionDocumentationTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionDocumentationTest.java
@@ -6,11 +6,14 @@
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.KeepConstraint;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
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.google.common.collect.ImmutableList;
+import java.lang.reflect.Field;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -19,7 +22,8 @@
@RunWith(Parameterized.class)
public class UsesReflectionDocumentationTest extends TestBase {
- static final String EXPECTED = StringUtils.lines("on Base", "on Sub");
+ static final String EXPECTED =
+ StringUtils.lines("on Base", "on Sub", "intField = 42", "stringField = Hello!");
private final TestParameters parameters;
@@ -35,7 +39,8 @@
@Test
public void testReference() throws Exception {
testForRuntime(parameters)
- .addProgramClasses(getInputClasses())
+ .addProgramClasses(TestClass.class)
+ .addProgramClassesAndInnerClasses(getExampleClasses())
.run(parameters.getRuntime(), TestClass.class)
.assertSuccessWithOutput(EXPECTED);
}
@@ -44,64 +49,122 @@
public void testWithRuleExtraction() throws Exception {
testForR8(parameters.getBackend())
.enableExperimentalKeepAnnotations()
- .addProgramClasses(getInputClasses())
+ .addProgramClasses(TestClass.class)
+ .addProgramClassesAndInnerClasses(getExampleClasses())
.addKeepMainRule(TestClass.class)
.setMinApi(parameters)
.run(parameters.getRuntime(), TestClass.class)
.assertSuccessWithOutput(EXPECTED);
}
- public List<Class<?>> getInputClasses() {
- return ImmutableList.of(TestClass.class, BaseClass.class, SubClass.class, MyClass.class);
+ public List<Class<?>> getExampleClasses() {
+ return ImmutableList.of(Example1.class, Example2.class);
}
- static class BaseClass {
- void hiddenMethod() {
- System.out.println("on Base");
+ 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 SubClass extends BaseClass {
- void hiddenMethod() {
- System.out.println("on Sub");
+ 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());
}
}
- /* 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 `foo` 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 */
-
- // INCLUDE CODE: UsesReflectionOnVirtualMethod
- static class MyClass {
-
- @UsesReflection({
- @KeepTarget(
- instanceOfClassConstant = BaseClass.class,
- methodName = "hiddenMethod",
- methodParameters = {})
- })
- public void foo(BaseClass base) throws Exception {
- base.getClass().getDeclaredMethod("hiddenMethod").invoke(base);
- }
- }
-
- // INCLUDE END
-
static class TestClass {
public static void main(String[] args) throws Exception {
- new MyClass().foo(new BaseClass());
- new MyClass().foo(new SubClass());
+ Example1.run();
+ Example2.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 6fd05eb..9088752 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,11 +7,16 @@
import static com.android.tools.r8.keepanno.utils.KeepItemAnnotationGenerator.quote;
import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.keepanno.annotations.FieldAccessFlags;
import com.android.tools.r8.keepanno.annotations.KeepBinding;
import com.android.tools.r8.keepanno.annotations.KeepCondition;
+import com.android.tools.r8.keepanno.annotations.KeepConstraint;
import com.android.tools.r8.keepanno.annotations.KeepEdge;
import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
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.UsedByNative;
import com.android.tools.r8.keepanno.annotations.UsedByReflection;
import com.android.tools.r8.keepanno.annotations.UsesReflection;
@@ -22,6 +27,8 @@
import com.android.tools.r8.utils.StringUtils;
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -69,6 +76,7 @@
this.generator = generator;
typeLinkReplacements =
getTypeLinkReplacements(
+ // Annotations.
KeepEdge.class,
KeepBinding.class,
KeepTarget.class,
@@ -76,7 +84,13 @@
UsesReflection.class,
UsedByReflection.class,
UsedByNative.class,
- KeepForApi.class);
+ KeepForApi.class,
+ // Enums.
+ KeepConstraint.class,
+ KeepItemKind.class,
+ MemberAccessFlags.class,
+ MethodAccessFlags.class,
+ FieldAccessFlags.class);
populateCodeAndDocReplacements(
UsesReflectionDocumentationTest.class, MainMethodsDocumentationTest.class);
}
@@ -84,7 +98,22 @@
private Map<String, String> getTypeLinkReplacements(Class<?>... classes) {
ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
for (Class<?> clazz : classes) {
- builder.put("`@" + clazz.getSimpleName() + "`", getMdLink(clazz));
+ String prefix = "`@" + clazz.getSimpleName();
+ String suffix = "`";
+ if (clazz.isAnnotation()) {
+ builder.put(prefix + suffix, getMdAnnotationLink(clazz));
+ for (Method method : clazz.getDeclaredMethods()) {
+ builder.put(
+ prefix + "#" + method.getName() + suffix, getMdAnnotationPropertyLink(method));
+ }
+ } else if (clazz.isEnum()) {
+ builder.put(prefix + suffix, getMdEnumLink(clazz));
+ for (Field field : clazz.getDeclaredFields()) {
+ builder.put(prefix + "#" + field.getName() + suffix, getMdEnumFieldLink(field));
+ }
+ } else {
+ throw new RuntimeException("Unexpected type of class for doc links");
+ }
}
return builder.build();
}
@@ -128,9 +157,30 @@
}
}
- private String getMdLink(Class<?> clazz) {
- String url = JAVADOC_URL + clazz.getTypeName().replace('.', '/') + ".html";
- return "[@" + clazz.getSimpleName() + "](" + url + ")";
+ private static String getClassJavaDocUrl(Class<?> clazz) {
+ return JAVADOC_URL + clazz.getTypeName().replace('.', '/') + ".html";
+ }
+
+ private String getMdAnnotationLink(Class<?> clazz) {
+ return "[@" + clazz.getSimpleName() + "](" + getClassJavaDocUrl(clazz) + ")";
+ }
+
+ private String getMdAnnotationPropertyLink(Method method) {
+ Class<?> clazz = method.getDeclaringClass();
+ String methodName = method.getName();
+ String url = getClassJavaDocUrl(clazz) + "#" + methodName + "()";
+ return "[@" + clazz.getSimpleName() + "." + methodName + "](" + url + ")";
+ }
+
+ private String getMdEnumLink(Class<?> clazz) {
+ return "[" + clazz.getSimpleName() + "](" + getClassJavaDocUrl(clazz) + ")";
+ }
+
+ private String getMdEnumFieldLink(Field field) {
+ Class<?> clazz = field.getDeclaringClass();
+ String fieldName = field.getName();
+ String url = getClassJavaDocUrl(clazz) + "#" + fieldName;
+ return "[" + clazz.getSimpleName() + "." + fieldName + "](" + url + ")";
}
private void println() {
diff --git a/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java b/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java
index d139b43..b372a72 100644
--- a/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java
+++ b/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java
@@ -12,6 +12,7 @@
import com.android.tools.r8.keepanno.annotations.FieldAccessFlags;
import com.android.tools.r8.keepanno.annotations.KeepBinding;
import com.android.tools.r8.keepanno.annotations.KeepCondition;
+import com.android.tools.r8.keepanno.annotations.KeepConstraint;
import com.android.tools.r8.keepanno.annotations.KeepEdge;
import com.android.tools.r8.keepanno.annotations.KeepForApi;
import com.android.tools.r8.keepanno.annotations.KeepItemKind;
@@ -22,6 +23,8 @@
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.utils.StringUtils;
+import com.android.tools.r8.utils.StringUtils.BraceType;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import java.io.ByteArrayOutputStream;
@@ -39,6 +42,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.function.Consumer;
+import org.jetbrains.annotations.NotNull;
public class KeepItemAnnotationGenerator {
@@ -259,7 +263,7 @@
}
private static String KIND_GROUP = "kind";
- private static String OPTIONS_GROUP = "options";
+ private static String CONSTRAINTS_GROUP = "constraints";
private static String CLASS_GROUP = "class";
private static String CLASS_NAME_GROUP = "class-name";
private static String INSTANCE_OF_GROUP = "instance-of";
@@ -344,39 +348,76 @@
.addUnorderedList(
KeepItemKind.ONLY_CLASS.name(),
KeepItemKind.ONLY_MEMBERS.name(),
- KeepItemKind.CLASS_AND_MEMBERS.name())
- .addParagraph(
- "If unspecified the default for an item with no member patterns is",
- KeepItemKind.ONLY_CLASS.name(),
- "and if it does have member patterns the default is",
- KeepItemKind.ONLY_MEMBERS.name());
+ KeepItemKind.ONLY_METHODS.name(),
+ KeepItemKind.ONLY_FIELDS.name(),
+ KeepItemKind.CLASS_AND_MEMBERS.name(),
+ KeepItemKind.CLASS_AND_METHODS.name(),
+ KeepItemKind.CLASS_AND_FIELDS.name())
+ .addParagraph("If unspecified the default for an item depends on its member patterns:")
+ .addUnorderedList(
+ KeepItemKind.ONLY_CLASS.name() + " if no member patterns are defined",
+ KeepItemKind.ONLY_METHODS.name() + " if method patterns are defined",
+ KeepItemKind.ONLY_FIELDS.name() + " if field patterns are defined",
+ KeepItemKind.ONLY_MEMBERS.name() + " otherwise.");
}
- private Group getKeepOptionsGroup() {
- return new Group(OPTIONS_GROUP)
+ private Group getKeepConstraintsGroup() {
+ return new Group(CONSTRAINTS_GROUP)
+ .addMember(constraints())
.addMember(
new GroupMember("allow")
- .setDocTitle("Define the " + OPTIONS_GROUP + " that are allowed to be modified.")
- .addParagraph("The specified options do not need to be preserved for the target.")
- .setDocReturn("Options allowed to be modified for the target.")
+ .setDeprecated("Use " + docLink(constraints()) + " instead.")
+ .setDocTitle(
+ "Define the " + CONSTRAINTS_GROUP + " that are allowed to be modified.")
+ .addParagraph(
+ "The specified option constraints do not need to be preserved for the"
+ + " target.")
+ .setDocReturn("Option constraints allowed to be modified for the target.")
.defaultEmptyArray("KeepOption"))
.addMember(
new GroupMember("disallow")
+ .setDeprecated("Use " + docLink(constraints()) + " instead.")
.setDocTitle(
- "Define the " + OPTIONS_GROUP + " that are not allowed to be modified.")
- .addParagraph("The specified options *must* be preserved for the target.")
- .setDocReturn("Options not allowed to be modified for the target.")
+ "Define the " + CONSTRAINTS_GROUP + " that are not allowed to be modified.")
+ .addParagraph(
+ "The specified option constraints *must* be preserved for the target.")
+ .setDocReturn("Option constraints not allowed to be modified for the target.")
.defaultEmptyArray("KeepOption"))
.addDocFooterParagraph(
"If nothing is specified for "
- + OPTIONS_GROUP
- + " the default is "
- + quote("allow none")
- + " / "
- + quote("disallow all")
+ + CONSTRAINTS_GROUP
+ + " the default is the default for "
+ + docLink(constraints())
+ ".");
}
+ private static String docLinkList(Enum<?>... values) {
+ return StringUtils.join(", ", values, v -> docLink(v), BraceType.TUBORG);
+ }
+
+ @NotNull
+ private static GroupMember constraints() {
+ return new GroupMember("constraints")
+ .setDocTitle("Define the usage constraints of the target.")
+ .addParagraph("The specified constraints must remain valid for the target.")
+ .addParagraph("The default constraints depend on the type of the target.")
+ .addUnorderedList(
+ "For classes, the default is "
+ + docLinkList(
+ KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.CLASS_INSTANTIATE),
+ "For methods, the default is "
+ + docLinkList(
+ KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.METHOD_INVOKE),
+ "For fields, the default is "
+ + docLinkList(
+ KeepConstraint.LOOKUP,
+ KeepConstraint.NAME,
+ KeepConstraint.FIELD_GET,
+ KeepConstraint.FIELD_SET))
+ .setDocReturn("Usage constraints for the target.")
+ .defaultEmptyArray(KeepConstraint.class);
+ }
+
private GroupMember bindingName() {
return new GroupMember("bindingName")
.setDocTitle(
@@ -740,7 +781,7 @@
() -> {
getKindGroup().generate(this);
println();
- getKeepOptionsGroup().generate(this);
+ getKeepConstraintsGroup().generate(this);
println();
generateClassAndMemberPropertiesWithClassAndMemberBinding();
});
@@ -963,20 +1004,20 @@
println("}");
}
- private String annoSimpleName(Class<?> clazz) {
+ private static String annoSimpleName(Class<?> clazz) {
return "@" + simpleName(clazz);
}
- private String docLink(Class<?> clazz) {
+ private static String docLink(Class<?> clazz) {
return "{@link " + simpleName(clazz) + "}";
}
- private String docLink(GroupMember member) {
+ private static String docLink(GroupMember member) {
return "{@link #" + member.name + "}";
}
- private String docLink(KeepItemKind kind) {
- return "{@link KeepItemKind#" + kind.name() + "}";
+ private static String docLink(Enum<?> kind) {
+ return "{@link " + simpleName(kind.getClass()) + "#" + kind.name() + "}";
}
private void generateConstants() {
@@ -1008,6 +1049,7 @@
generateConditionConstants();
generateTargetConstants();
generateKindConstants();
+ generateConstraintConstants();
generateOptionConstants();
generateMemberAccessConstants();
generateMethodAccessConstants();
@@ -1163,7 +1205,7 @@
() -> {
generateAnnotationConstants(KeepTarget.class);
getKindGroup().generateConstants(this);
- getKeepOptionsGroup().generateConstants(this);
+ getKeepConstraintsGroup().generateConstants(this);
});
println("}");
println();
@@ -1189,6 +1231,20 @@
println();
}
+ private void generateConstraintConstants() {
+ println("public static final class Constraints {");
+ withIndent(
+ () -> {
+ generateAnnotationConstants(KeepConstraint.class);
+ for (KeepConstraint value : KeepConstraint.values()) {
+ println(
+ "public static final String " + value.name() + " = " + quote(value.name()) + ";");
+ }
+ });
+ println("}");
+ println();
+ }
+
private void generateOptionConstants() {
println("public static final class Option {");
withIndent(