[KeepAnno] Introduce extraction options to support other shrinkers

Bug: b/321674067
Change-Id: I92fedbf06d10a3fa42a5da6cafc1931bc7c5b0db
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
index 7149900..1c554fc 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
@@ -49,10 +49,16 @@
 /** Extract the PG keep rules that over-approximate a keep edge. */
 public class KeepRuleExtractor {
 
+  private final KeepRuleExtractorOptions options;
   private final Consumer<String> ruleConsumer;
 
   public KeepRuleExtractor(Consumer<String> ruleConsumer) {
+    this(ruleConsumer, KeepRuleExtractorOptions.getR8Options());
+  }
+
+  public KeepRuleExtractor(Consumer<String> ruleConsumer, KeepRuleExtractorOptions options) {
     this.ruleConsumer = ruleConsumer;
+    this.options = options;
   }
 
   public void extract(KeepDeclaration declaration) {
@@ -60,7 +66,7 @@
     PgRule.groupByKinds(rules);
     StringBuilder builder = new StringBuilder();
     for (PgRule rule : rules) {
-      rule.printRule(builder);
+      rule.printRule(builder, options);
       builder.append("\n");
     }
     ruleConsumer.accept(builder.toString());
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractorOptions.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractorOptions.java
new file mode 100644
index 0000000..e428358
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractorOptions.java
@@ -0,0 +1,47 @@
+// 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.keeprules;
+
+import com.android.tools.r8.keepanno.ast.KeepOptions.KeepOption;
+
+public class KeepRuleExtractorOptions {
+
+  private static final KeepRuleExtractorOptions PG_OPTIONS = new KeepRuleExtractorOptions(false);
+  private static final KeepRuleExtractorOptions R8_OPTIONS = new KeepRuleExtractorOptions(true);
+
+  public static KeepRuleExtractorOptions getPgOptions() {
+    return PG_OPTIONS;
+  }
+
+  public static KeepRuleExtractorOptions getR8Options() {
+    return R8_OPTIONS;
+  }
+
+  private final boolean allowAccessModificationOption;
+  private final boolean allowAnnotationRemovalOption = false;
+
+  private KeepRuleExtractorOptions(boolean allowAccessModificationOption) {
+    this.allowAccessModificationOption = allowAccessModificationOption;
+  }
+
+  private boolean hasAllowAccessModificationOptionSupport() {
+    return allowAccessModificationOption;
+  }
+
+  private boolean hasAllowAnnotationRemovalOptionSupport() {
+    return allowAnnotationRemovalOption;
+  }
+
+  public boolean isKeepOptionSupported(KeepOption keepOption) {
+    switch (keepOption) {
+      case ACCESS_MODIFICATION:
+        return hasAllowAccessModificationOptionSupport();
+      case ANNOTATION_REMOVAL:
+        return hasAllowAnnotationRemovalOptionSupport();
+      default:
+        return true;
+    }
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/PgRule.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/PgRule.java
index c329c34..75ea20c 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/PgRule.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/PgRule.java
@@ -114,14 +114,14 @@
     };
   }
 
-  void printKeepOptions(StringBuilder builder) {
-    RulePrintingUtils.printKeepOptions(builder, options);
+  void printKeepOptions(StringBuilder builder, KeepRuleExtractorOptions extractorOptions) {
+    RulePrintingUtils.printKeepOptions(builder, options, extractorOptions);
   }
 
-  public void printRule(StringBuilder builder) {
+  public void printRule(StringBuilder builder, KeepRuleExtractorOptions extractorOptions) {
     RulePrintingUtils.printHeader(builder, metaInfo);
     printCondition(builder);
-    printConsequence(builder);
+    printConsequence(builder, extractorOptions);
   }
 
   void printCondition(StringBuilder builder) {
@@ -141,9 +141,9 @@
     }
   }
 
-  void printConsequence(StringBuilder builder) {
+  void printConsequence(StringBuilder builder, KeepRuleExtractorOptions extractorOptions) {
     builder.append(getConsequenceKeepType());
-    printKeepOptions(builder);
+    printKeepOptions(builder, extractorOptions);
     builder.append(' ');
     printTargetHolder(builder);
     List<KeepBindingSymbol> members = getTargetMembers();
@@ -249,7 +249,7 @@
     }
 
     @Override
-    public void printRule(StringBuilder builder) {
+    public void printRule(StringBuilder builder, KeepRuleExtractorOptions options) {
       RulePrintingUtils.printHeader(builder, getMetaInfo());
       builder.append(getConsequenceKeepType()).append(" ");
       List<KeepAttribute> sorted = new ArrayList<>(attributes);
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 2bda0ec..636dbfc 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
@@ -84,13 +84,10 @@
     return string;
   }
 
-  public static void printKeepOptions(StringBuilder builder, KeepOptions options) {
+  public static void printKeepOptions(
+      StringBuilder builder, KeepOptions options, KeepRuleExtractorOptions extractorOptions) {
     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)) {
+      if (options.isAllowed(option) && extractorOptions.isKeepOptionSupported(option)) {
         builder.append(",allow").append(getOptionString(option));
       }
     }
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestUtils.java b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestUtils.java
new file mode 100644
index 0000000..7f79344
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestUtils.java
@@ -0,0 +1,92 @@
+// 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;
+
+import com.android.tools.r8.ByteDataView;
+import com.android.tools.r8.ClassFileConsumer.ArchiveConsumer;
+import com.android.tools.r8.ExternalR8TestBuilder;
+import com.android.tools.r8.ProguardTestBuilder;
+import com.android.tools.r8.ProguardVersion;
+import com.android.tools.r8.TestShrinkerBuilder;
+import com.android.tools.r8.ThrowableConsumer;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.keepanno.asm.KeepEdgeReader;
+import com.android.tools.r8.keepanno.ast.KeepDeclaration;
+import com.android.tools.r8.keepanno.keeprules.KeepRuleExtractor;
+import com.android.tools.r8.keepanno.keeprules.KeepRuleExtractorOptions;
+import com.android.tools.r8.utils.FileUtils;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+import org.junit.rules.TemporaryFolder;
+
+public class KeepAnnoTestUtils {
+
+  // TODO(b/321674067): Update this to PG 7.4.
+  public static ProguardVersion PG_VERSION = ProguardVersion.V7_3_2;
+
+  // TODO(b/321674067): Downgrade this to oldest supported AGP, such as R8 8.0.35.
+  private static Path R8_LIB = Paths.get(ToolHelper.THIRD_PARTY_DIR, "r8", "r8lib_8.2.20-dev.jar");
+
+  public static Path getKeepAnnoLib(TemporaryFolder temp) throws IOException {
+    Path archive = temp.newFolder().toPath().resolve("keepanno.jar");
+    Path root = ToolHelper.getKeepAnnoPath();
+    ArchiveConsumer consumer = new ArchiveConsumer(archive);
+    Path annoDir =
+        root.resolve(Paths.get("com", "android", "tools", "r8", "keepanno", "annotations"));
+    try (Stream<Path> paths = Files.list(annoDir)) {
+      paths.forEach(
+          p -> {
+            if (FileUtils.isClassFile(p)) {
+              byte[] data = FileUtils.uncheckedReadAllBytes(p);
+              String fileName = p.getFileName().toString();
+              String className = fileName.substring(0, fileName.lastIndexOf('.'));
+              String desc = "Lcom/android/tools/r8/keepanno/annotations/" + className + ";";
+              consumer.accept(ByteDataView.of(data), desc, null);
+            }
+          });
+    }
+    consumer.finished(null);
+    return archive;
+  }
+
+  public static List<String> extractRules(
+      List<Class<?>> inputClasses, KeepRuleExtractorOptions extractorOptions) throws IOException {
+    List<String> rules = new ArrayList<>();
+    for (Class<?> inputClass : inputClasses) {
+      byte[] bytes = ToolHelper.getClassAsBytes(inputClass);
+      List<KeepDeclaration> declarations = KeepEdgeReader.readKeepEdges(bytes);
+      KeepRuleExtractor extractor = new KeepRuleExtractor(rules::add, extractorOptions);
+      declarations.forEach(extractor::extract);
+    }
+    return rules;
+  }
+
+  public static ThrowableConsumer<ProguardTestBuilder> addInputClassesAndRulesPG(
+      List<Class<?>> inputClasses) {
+    return builder -> {
+      addInputClassesAndRulesShared(inputClasses, builder);
+    };
+  }
+
+  public static ThrowableConsumer<ExternalR8TestBuilder> addInputClassesAndRulesR8(
+      List<Class<?>> inputClasses) {
+    return builder -> {
+      builder.useProvidedR8(R8_LIB);
+      addInputClassesAndRulesShared(inputClasses, builder);
+    };
+  }
+
+  private static void addInputClassesAndRulesShared(
+      List<Class<?>> inputClasses, TestShrinkerBuilder<?, ?, ?, ?, ?> builder) throws IOException {
+    Path keepAnnoLib = getKeepAnnoLib(builder.getState().getTempFolder());
+    List<String> rules = extractRules(inputClasses, KeepRuleExtractorOptions.getPgOptions());
+    builder.addProgramClasses(inputClasses).addProgramFiles(keepAnnoLib).addKeepRules(rules);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepUsedByReflectionAnnotationTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepUsedByReflectionAnnotationTest.java
index 210cea0..e6edebc 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepUsedByReflectionAnnotationTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepUsedByReflectionAnnotationTest.java
@@ -6,7 +6,9 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assume.assumeTrue;
 
+import com.android.tools.r8.ProguardVersion;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -60,6 +62,30 @@
   }
 
   @Test
+  public void testExtractR8() throws Exception {
+    testForExternalR8(parameters.getBackend())
+        .apply(KeepAnnoTestUtils.addInputClassesAndRulesR8(getInputClasses()))
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkOutput);
+  }
+
+  @Test
+  public void testExtractPG() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForProguard(KeepAnnoTestUtils.PG_VERSION)
+        .addDontWarn(getClass())
+        .apply(KeepAnnoTestUtils.addInputClassesAndRulesPG(getInputClasses()))
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkOutput);
+  }
+
+  @Test
   public void testNoRefReference() throws Exception {
     testForRuntime(parameters)
         .addProgramClasses(getInputClasses())
@@ -80,6 +106,31 @@
         .inspect(this::checkOutputNoRef);
   }
 
+  @Test
+  public void testNoRefExtractR8() throws Exception {
+    testForExternalR8(parameters.getBackend())
+        .apply(KeepAnnoTestUtils.addInputClassesAndRulesR8(getInputClasses()))
+        .addKeepMainRule(TestClassNoRef.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClassNoRef.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkOutputNoRef);
+  }
+
+  @Test
+  public void testNoRefExtractPG() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForProguard(ProguardVersion.V7_3_2)
+        .addDontWarn(getClass())
+        .apply(KeepAnnoTestUtils.addInputClassesAndRulesPG(getInputClasses()))
+        .addKeepMainRule(TestClassNoRef.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClassNoRef.class)
+        .assertSuccessWithOutput(EXPECTED)
+        // PG does not eliminate B so the same output remains.
+        .inspect(this::checkOutput);
+  }
+
   public List<Class<?>> getInputClasses() {
     return ImmutableList.of(TestClass.class, TestClassNoRef.class, A.class, B.class, C.class);
   }