[KeepAnno] Add user guide for @KeepForApi
Bug: b/248408342
Change-Id: I89d9fb871a51ef59a2bf8bd215eef1073778ee27
diff --git a/doc/keepanno-guide.md b/doc/keepanno-guide.md
index d91f5b1..b816d6d 100644
--- a/doc/keepanno-guide.md
+++ b/doc/keepanno-guide.md
@@ -172,7 +172,7 @@
```
-public static class MyClassWithFields implements PrintableFieldInterface {
+public class MyClassWithFields implements PrintableFieldInterface {
@UsedByReflection final int intField = 42;
@UsedByReflection String stringField = "Hello!";
@@ -197,7 +197,7 @@
@UsedByReflection(
kind = KeepItemKind.ONLY_FIELDS,
constraints = {KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.FIELD_GET})
-public static class MyClassWithFields implements PrintableFieldInterface {
+public class MyClassWithFields implements PrintableFieldInterface {
final int intField = 42;
String stringField = "Hello!";
}
@@ -224,7 +224,7 @@
},
kind = KeepItemKind.ONLY_FIELDS,
constraints = {KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.FIELD_GET})
-public static class MyClassWithFields implements PrintableFieldInterface {
+public class MyClassWithFields implements PrintableFieldInterface {
final int intField = 42;
String stringField = "Hello!";
}
@@ -234,7 +234,68 @@
## Annotating APIs<a id="apis"></a>
-TODO
+If your code is being shrunk before release as a library, or if you have an API
+surface that is used via dynamic loading at runtime, then you need to keep the
+API surface. For that you should use the [@KeepForApi](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepForApi.html) annotation.
+
+When annotating a class the default for [@KeepForApi](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepForApi.html) is to keep the class as well as all of its
+public and protected members:
+
+
+```
+@KeepForApi
+public class MyApi {
+ public void thisPublicMethodIsKept() {
+ /* ... */
+ }
+
+ protected void thisProtectedMethodIsKept() {
+ /* ... */
+ }
+
+ void thisPackagePrivateMethodIsNotKept() {
+ /* ... */
+ }
+
+ private void thisPrivateMethodIsNotKept() {
+ /* ... */
+ }
+}
+```
+
+
+The default can be changed using the [@KeepForApi.memberAccess](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepForApi.html#memberAccess()) property:
+
+
+```
+@KeepForApi(
+ memberAccess = {
+ MemberAccessFlags.PUBLIC,
+ MemberAccessFlags.PROTECTED,
+ MemberAccessFlags.PACKAGE_PRIVATE
+ })
+```
+
+
+The [@KeepForApi](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepForApi.html) annotation can also be placed directly on members and avoid keeping
+unannotated members. The holder class is implicitly kept. When annotating the members
+directly, the access does not matter as illustrated here by annotating a package private method:
+
+
+```
+public class MyOtherApi {
+
+ public void notKept() {
+ /* ... */
+ }
+
+ @KeepForApi
+ void isKept() {
+ /* ... */
+ }
+}
+```
+
## Migrating rules to annotations<a id="migrating-rules"></a>
diff --git a/doc/keepanno-guide.template.md b/doc/keepanno-guide.template.md
index 7b16a54..104bf02 100644
--- a/doc/keepanno-guide.template.md
+++ b/doc/keepanno-guide.template.md
@@ -108,7 +108,21 @@
## [Annotating APIs](apis)
-TODO
+If your code is being shrunk before release as a library, or if you have an API
+surface that is used via dynamic loading at runtime, then you need to keep the
+API surface. For that you should use the `@KeepForApi` annotation.
+
+[[[INCLUDE DOC:ApiClass]]]
+
+[[[INCLUDE CODE:ApiClass]]]
+
+[[[INCLUDE DOC:ApiClassMemberAccess]]]
+
+[[[INCLUDE CODE:ApiClassMemberAccess]]]
+
+[[[INCLUDE DOC:ApiMember]]]
+
+[[[INCLUDE CODE:ApiMember]]]
## [Migrating rules to annotations](migrating-rules)
diff --git a/src/test/java/com/android/tools/r8/keepanno/doctests/ConditionalMethodRulesAndHorizontalMergingTest.java b/src/test/java/com/android/tools/r8/keepanno/ConditionalMethodRulesAndHorizontalMergingTest.java
similarity index 88%
rename from src/test/java/com/android/tools/r8/keepanno/doctests/ConditionalMethodRulesAndHorizontalMergingTest.java
rename to src/test/java/com/android/tools/r8/keepanno/ConditionalMethodRulesAndHorizontalMergingTest.java
index ee1748d..8bf2e6a 100644
--- a/src/test/java/com/android/tools/r8/keepanno/doctests/ConditionalMethodRulesAndHorizontalMergingTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/ConditionalMethodRulesAndHorizontalMergingTest.java
@@ -1,15 +1,15 @@
-// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.keepanno.doctests;
+package com.android.tools.r8.keepanno;
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.keepanno.doctests.ConditionalMethodRulesAndHorizontalMergingTest.Example1.BaseClass;
-import com.android.tools.r8.keepanno.doctests.ConditionalMethodRulesAndHorizontalMergingTest.Example1.MyHiddenMethodCaller;
-import com.android.tools.r8.keepanno.doctests.ConditionalMethodRulesAndHorizontalMergingTest.Example2.MyFieldValuePrinter;
-import com.android.tools.r8.keepanno.doctests.ConditionalMethodRulesAndHorizontalMergingTest.Example2.PrintableFieldInterface;
+import com.android.tools.r8.keepanno.ConditionalMethodRulesAndHorizontalMergingTest.Example1.BaseClass;
+import com.android.tools.r8.keepanno.ConditionalMethodRulesAndHorizontalMergingTest.Example1.MyHiddenMethodCaller;
+import com.android.tools.r8.keepanno.ConditionalMethodRulesAndHorizontalMergingTest.Example2.MyFieldValuePrinter;
+import com.android.tools.r8.keepanno.ConditionalMethodRulesAndHorizontalMergingTest.Example2.PrintableFieldInterface;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.StringUtils;
import com.google.common.collect.ImmutableList;
diff --git a/src/test/java/com/android/tools/r8/keepanno/doctests/ForApiDocumentationTest.java b/src/test/java/com/android/tools/r8/keepanno/doctests/ForApiDocumentationTest.java
new file mode 100644
index 0000000..7df53d6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/doctests/ForApiDocumentationTest.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.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.KeepForApi;
+import com.android.tools.r8.keepanno.annotations.MemberAccessFlags;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.lang.reflect.Method;
+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 ForApiDocumentationTest extends TestBase {
+
+ static final String EXPECTED_1_REF =
+ StringUtils.joinLines(
+ "thisPackagePrivateMethodIsNotKept",
+ "thisPrivateMethodIsNotKept",
+ "thisProtectedMethodIsKept",
+ "thisPublicMethodIsKept");
+
+ static final String EXPECTED_1_R8 =
+ StringUtils.joinLines("thisProtectedMethodIsKept", "thisPublicMethodIsKept");
+
+ static final String EXPECTED_2_REF =
+ StringUtils.joinLines(
+ "thisPackagePrivateMethodIsKept",
+ "thisPrivateMethodIsNotKept",
+ "thisProtectedMethodIsKept",
+ "thisPublicMethodIsKept");
+
+ static final String EXPECTED_2_R8 =
+ StringUtils.joinLines(
+ "thisPackagePrivateMethodIsKept", "thisProtectedMethodIsKept", "thisPublicMethodIsKept");
+
+ static final String EXPECTED_3_REF = StringUtils.joinLines("isKept", "notKept");
+
+ static final String EXPECTED_3_R8 = StringUtils.joinLines("isKept");
+
+ static final String EXPECTED_REF =
+ StringUtils.lines(EXPECTED_1_REF, EXPECTED_2_REF, EXPECTED_3_REF);
+
+ static final String EXPECTED_R8 = StringUtils.lines(EXPECTED_1_R8, EXPECTED_2_R8, EXPECTED_3_R8);
+
+ private final TestParameters parameters;
+
+ @Parameterized.Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+ }
+
+ public ForApiDocumentationTest(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_REF);
+ }
+
+ @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_R8);
+ }
+
+ public List<Class<?>> getExampleClasses() {
+ return ImmutableList.of(Example1.class, Example2.class, Example3.class);
+ }
+
+ static class Example1 {
+
+ /* INCLUDE DOC: ApiClass
+ When annotating a class the default for `@KeepForApi` is to keep the class as well as all of its
+ public and protected members:
+ INCLUDE END */
+
+ static
+ // INCLUDE CODE: ApiClass
+ @KeepForApi
+ public class MyApi {
+ public void thisPublicMethodIsKept() {
+ /* ... */
+ }
+
+ protected void thisProtectedMethodIsKept() {
+ /* ... */
+ }
+
+ void thisPackagePrivateMethodIsNotKept() {
+ /* ... */
+ }
+
+ private void thisPrivateMethodIsNotKept() {
+ /* ... */
+ }
+ }
+
+ // INCLUDE END
+
+ static void run() throws Exception {
+ TestClass.printMethods(MyApi.class);
+ }
+ }
+
+ static class Example2 {
+
+ /* INCLUDE DOC: ApiClassMemberAccess
+ The default can be changed using the `@KeepForApi#memberAccess` property:
+ INCLUDE END */
+
+ // INCLUDE CODE: ApiClassMemberAccess
+ @KeepForApi(
+ memberAccess = {
+ MemberAccessFlags.PUBLIC,
+ MemberAccessFlags.PROTECTED,
+ MemberAccessFlags.PACKAGE_PRIVATE
+ })
+ // INCLUDE END
+ public static class MyApi {
+ public void thisPublicMethodIsKept() {
+ /* ... */
+ }
+
+ protected void thisProtectedMethodIsKept() {
+ /* ... */
+ }
+
+ void thisPackagePrivateMethodIsKept() {
+ /* ... */
+ }
+
+ private void thisPrivateMethodIsNotKept() {
+ /* ... */
+ }
+ }
+
+ static void run() throws Exception {
+ TestClass.printMethods(MyApi.class);
+ }
+ }
+
+ static class Example3 {
+
+ /* INCLUDE DOC: ApiMember
+ The `@KeepForApi` annotation can also be placed directly on members and avoid keeping
+ unannotated members. The holder class is implicitly kept. When annotating the members
+ directly, the access does not matter as illustrated here by annotating a package private method:
+ INCLUDE END */
+
+ static
+ // INCLUDE CODE: ApiMember
+ public class MyOtherApi {
+
+ public void notKept() {
+ /* ... */
+ }
+
+ @KeepForApi
+ void isKept() {
+ /* ... */
+ }
+ }
+
+ // INCLUDE END
+
+ static void run() throws Exception {
+ TestClass.printMethods(MyOtherApi.class);
+ }
+ }
+
+ static class TestClass {
+
+ public static void main(String[] args) throws Exception {
+ Example1.run();
+ Example2.run();
+ Example3.run();
+ }
+
+ static void printMethods(Class<?> clazz) {
+ List<String> names = new ArrayList<>();
+ for (Method m : clazz.getDeclaredMethods()) {
+ names.add(m.getName());
+ }
+ names.sort(String::compareTo);
+ for (String name : names) {
+ System.out.println(name);
+ }
+ }
+ }
+}
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 034ac0a..98a2f0d 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
@@ -187,8 +187,9 @@
We elide it here for brevity.
INCLUDE END */
+ static
// INCLUDE CODE: UsedByReflectionFieldPrinterOnFields
- public static class MyClassWithFields implements PrintableFieldInterface {
+ public class MyClassWithFields implements PrintableFieldInterface {
@UsedByReflection final int intField = 42;
@UsedByReflection String stringField = "Hello!";
@@ -224,11 +225,12 @@
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 static class MyClassWithFields implements PrintableFieldInterface {
+ public class MyClassWithFields implements PrintableFieldInterface {
final int intField = 42;
String stringField = "Hello!";
}
@@ -265,6 +267,7 @@
Luckily we can specify the same precondition using `@UsedByReflection#preconditions`.
INCLUDE END */
+ static
// INCLUDE CODE: UsedByReflectionFieldPrinterConditional
@UsedByReflection(
preconditions = {
@@ -274,7 +277,7 @@
},
kind = KeepItemKind.ONLY_FIELDS,
constraints = {KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.FIELD_GET})
- public static class MyClassWithFields implements PrintableFieldInterface {
+ public class MyClassWithFields implements PrintableFieldInterface {
final int intField = 42;
String stringField = "Hello!";
}
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 9088752..d8b5730 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
@@ -20,6 +20,7 @@
import com.android.tools.r8.keepanno.annotations.UsedByNative;
import com.android.tools.r8.keepanno.annotations.UsedByReflection;
import com.android.tools.r8.keepanno.annotations.UsesReflection;
+import com.android.tools.r8.keepanno.doctests.ForApiDocumentationTest;
import com.android.tools.r8.keepanno.doctests.MainMethodsDocumentationTest;
import com.android.tools.r8.keepanno.doctests.UsesReflectionDocumentationTest;
import com.android.tools.r8.keepanno.utils.KeepItemAnnotationGenerator.Generator;
@@ -92,7 +93,9 @@
MethodAccessFlags.class,
FieldAccessFlags.class);
populateCodeAndDocReplacements(
- UsesReflectionDocumentationTest.class, MainMethodsDocumentationTest.class);
+ UsesReflectionDocumentationTest.class,
+ ForApiDocumentationTest.class,
+ MainMethodsDocumentationTest.class);
}
private Map<String, String> getTypeLinkReplacements(Class<?>... classes) {