[KeepAnno] Allow multiple extracted edges per type

Bug: b/323815449
Change-Id: I40f0f4f9037ac749626d861fea63703700492602
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/ExtractedKeepAnnotation.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/ExtractedKeepAnnotation.java
new file mode 100644
index 0000000..8b9d95c
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/ExtractedKeepAnnotation.java
@@ -0,0 +1,30 @@
+// 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.annotations;
+
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.CLASS)
+@Repeatable(ExtractedKeepAnnotations.class)
+public @interface ExtractedKeepAnnotation {
+  /**
+   * The version defining this extracted keep annotation.
+   *
+   * <p>Note: this version property must be the first property defined. Its content may determine
+   * the subsequent parsing.
+   */
+  String version();
+
+  /**
+   * The context giving rise to this extracted keep annotation.
+   *
+   * <p>The context must be a class descriptor, method descriptor or field descriptor.
+   */
+  String context();
+
+  /** The extracted edge. */
+  KeepEdge edge();
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/ExtractedKeepAnnotations.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/ExtractedKeepAnnotations.java
index 43a91bb..f6855cc 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/ExtractedKeepAnnotations.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/ExtractedKeepAnnotations.java
@@ -8,24 +8,15 @@
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
+/**
+ * Collection of extracted keep annotations.
+ *
+ * <p>This annotation is just a collection of the extracted annotations. It is version independent
+ * and is assumed to never change. Any version specific changes are to be made within the single
+ * element structure of {@link ExtractedKeepAnnotation}.
+ */
 @Target({ElementType.TYPE})
 @Retention(RetentionPolicy.CLASS)
 public @interface ExtractedKeepAnnotations {
-  /**
-   * The version of defining this extracted keep annotation.
-   *
-   * <p>Note: this version property must be the first property defined. Its content may determine
-   * the subsequent parsing.
-   */
-  String version();
-
-  /**
-   * The context giving rise to this extracted keep annotation.
-   *
-   * <p>The context must be a class descriptor, method descriptor or field descriptor.
-   */
-  String context();
-
-  /** The extracted edges. */
-  KeepEdge[] edges();
+  ExtractedKeepAnnotation[] value();
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
index e1c5ace..91d9c2e 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
@@ -13,7 +13,8 @@
 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.Edge;
-import com.android.tools.r8.keepanno.ast.AnnotationConstants.Extracted;
+import com.android.tools.r8.keepanno.ast.AnnotationConstants.ExtractedAnnotation;
+import com.android.tools.r8.keepanno.ast.AnnotationConstants.ExtractedAnnotations;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.FieldAccess;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.ForApi;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Item;
@@ -235,8 +236,9 @@
       if (visible) {
         return null;
       }
-      if (readExtracted && descriptor.equals(Extracted.DESCRIPTOR)) {
-        return new ExtractedAnnotationVisitor(annotationParsingContext(descriptor), parent::accept);
+      if (readExtracted && descriptor.equals(ExtractedAnnotations.DESCRIPTOR)) {
+        return new ExtractedAnnotationsVisitor(
+            annotationParsingContext(descriptor), parent::accept);
       }
       if (!readEmbedded) {
         return null;
@@ -527,12 +529,52 @@
     }
   }
 
+  private static class ExtractedAnnotationsVisitor extends AnnotationVisitorBase {
+
+    private final Parent<KeepDeclaration> parent;
+    private List<KeepDeclaration> declarations = new ArrayList<>();
+
+    public ExtractedAnnotationsVisitor(
+        AnnotationParsingContext parsingContext, Parent<KeepDeclaration> parent) {
+      super(parsingContext);
+      this.parent = parent;
+    }
+
+    @Override
+    public AnnotationVisitor visitArray(String name) {
+      if (name.equals(ExtractedAnnotations.value)) {
+        PropertyParsingContext parsingContext = getParsingContext().property(name);
+        return new AnnotationVisitorBase(parsingContext) {
+          @Override
+          public AnnotationVisitor visitAnnotation(String nullName, String descriptor) {
+            assert nullName == null;
+            if (descriptor.equals(ExtractedAnnotation.DESCRIPTOR)) {
+              return new ExtractedAnnotationVisitor(
+                  parsingContext.annotation(descriptor), declarations::add);
+            }
+            return super.visitAnnotation(nullName, descriptor);
+          }
+        };
+      }
+      return super.visitArray(name);
+    }
+
+    @Override
+    public void visitEnd() {
+      if (declarations.isEmpty()) {
+        throw new KeepEdgeException("Invalid extracted annotation set, expected non-empty.");
+      }
+      declarations.forEach(parent::accept);
+      super.visitEnd();
+    }
+  }
+
   private static class ExtractedAnnotationVisitor extends AnnotationVisitorBase {
 
     private final Parent<KeepDeclaration> parent;
     private String context = null;
     private String version = null;
-    private List<KeepEdgeVisitor> edgeVisitors = new ArrayList<>();
+    private KeepEdgeVisitor edgeVisitor = null;
 
     public ExtractedAnnotationVisitor(
         AnnotationParsingContext parsingContext, Parent<KeepDeclaration> parent) {
@@ -548,12 +590,12 @@
 
     @Override
     public void visit(String name, Object value) {
-      if (name.equals(Extracted.version) && value instanceof String) {
+      if (name.equals(ExtractedAnnotation.version) && value instanceof String) {
         version = (String) value;
         return;
       }
       ensureVersion(getParsingContext().property(name));
-      if (name.equals(Extracted.context) && value instanceof String) {
+      if (name.equals(ExtractedAnnotation.context) && value instanceof String) {
         context = (String) value;
         return;
       }
@@ -561,48 +603,37 @@
     }
 
     @Override
-    public AnnotationVisitor visitArray(String name) {
-      if (name.equals(Extracted.edges)) {
-        PropertyParsingContext parsingContext = getParsingContext().property(name);
-        ensureVersion(parsingContext);
-        return new AnnotationVisitorBase(parsingContext) {
-          @Override
-          public AnnotationVisitor visitAnnotation(String nullName, String descriptor) {
-            assert nullName == null;
-            if (descriptor.equals(Edge.DESCRIPTOR)) {
-              KeepEdgeVisitor visitor =
-                  new KeepEdgeVisitor(
-                      parsingContext.annotation(descriptor), edge -> {}, builder -> {});
-              edgeVisitors.add(visitor);
-              return visitor;
-            }
-            return super.visitAnnotation(nullName, descriptor);
-          }
-        };
+    public AnnotationVisitor visitAnnotation(String name, String descriptor) {
+      if (name.equals(ExtractedAnnotation.edge) && descriptor.equals(Edge.DESCRIPTOR)) {
+        edgeVisitor =
+            new KeepEdgeVisitor(
+                getParsingContext().annotation(descriptor), edge -> {}, builder -> {});
+        return edgeVisitor;
       }
-      return super.visitArray(name);
+      return super.visitAnnotation(name, descriptor);
     }
 
     @Override
     public void visitEnd() {
       if (version == null) {
-        throw new KeepEdgeException("Invalid extracted edge, expected a version property.");
+        throw new KeepEdgeException("Invalid extracted annotation, expected a version property.");
       }
       if (context == null) {
-        throw new KeepEdgeException("Invalid extracted edge, expected a context property.");
+        throw new KeepEdgeException("Invalid extracted annotation, expected a context property.");
       }
-      for (KeepEdgeVisitor visitor : edgeVisitors) {
-        parent.accept(
-            visitor
-                .builder
-                .setMetaInfo(
-                    visitor
-                        .metaInfoBuilder
-                        // TODO(b/323815449): This may be a method or field descriptor!
-                        .setContextFromClassDescriptor(context)
-                        .build())
-                .build());
+      if (edgeVisitor == null) {
+        throw new KeepEdgeException("Invalid extracted annotation, expected an edge property.");
       }
+      parent.accept(
+          edgeVisitor
+              .builder
+              .setMetaInfo(
+                  edgeVisitor
+                      .metaInfoBuilder
+                      // TODO(b/323815449): This may be a method or field descriptor!
+                      .setContextFromClassDescriptor(context)
+                      .build())
+              .build());
       super.visitEnd();
     }
   }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
index 8145c50..dbbfa7c 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
@@ -11,7 +11,8 @@
 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.Extracted;
+import com.android.tools.r8.keepanno.ast.AnnotationConstants.ExtractedAnnotation;
+import com.android.tools.r8.keepanno.ast.AnnotationConstants.ExtractedAnnotations;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.FieldAccess;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Item;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Kind;
@@ -28,6 +29,7 @@
 import com.android.tools.r8.keepanno.ast.KeepConsequences;
 import com.android.tools.r8.keepanno.ast.KeepConstraint;
 import com.android.tools.r8.keepanno.ast.KeepConstraints;
+import com.android.tools.r8.keepanno.ast.KeepDeclaration;
 import com.android.tools.r8.keepanno.ast.KeepEdge;
 import com.android.tools.r8.keepanno.ast.KeepEdgeMetaInfo;
 import com.android.tools.r8.keepanno.ast.KeepFieldAccessPattern;
@@ -121,19 +123,35 @@
     visitor.visitEnd();
   }
 
-  public static void writeExtractedEdge(
-      KeepEdge edge, BiFunction<String, Boolean, AnnotationVisitorInterface> getVisitor) {
+  public static void writeExtractedEdges(
+      List<KeepDeclaration> declarations,
+      BiFunction<String, Boolean, AnnotationVisitorInterface> getVisitor) {
+    if (declarations.isEmpty()) {
+      return;
+    }
     withNewVisitor(
-        wrap(getVisitor.apply(Extracted.DESCRIPTOR, false)),
-        extractVisitor -> {
-          extractVisitor.visit("version", edge.getMetaInfo().getVersion().toVersionString());
-          extractVisitor.visit("context", edge.getMetaInfo().getContextDescriptorString());
-          withNewVisitor(
-              extractVisitor.visitArray("edges"),
-              edgeVisitor ->
-                  writeEdgeInternal(
-                      edge, (desc, visible) -> edgeVisitor.visitAnnotation(null, desc)));
-        });
+        wrap(getVisitor.apply(ExtractedAnnotations.DESCRIPTOR, false)),
+        containerVisitor ->
+            withNewVisitor(
+                containerVisitor.visitArray(ExtractedAnnotations.value),
+                arrayVisitor ->
+                    declarations.forEach(
+                        decl ->
+                            withNewVisitor(
+                                arrayVisitor.visitAnnotation(null, ExtractedAnnotation.DESCRIPTOR),
+                                extractVisitor -> writeExtractedEdge(extractVisitor, decl)))));
+  }
+
+  private static void writeExtractedEdge(AnnotationVisitor visitor, KeepDeclaration decl) {
+    if (decl.isKeepCheck()) {
+      throw new Unimplemented("Checks not yet supported for extraction");
+    }
+    KeepEdgeMetaInfo metaInfo = decl.getMetaInfo();
+    visitor.visit(ExtractedAnnotation.version, metaInfo.getVersion().toVersionString());
+    visitor.visit(ExtractedAnnotation.context, metaInfo.getContextDescriptorString());
+    writeEdgeInternal(
+        decl.asKeepEdge(),
+        (desc, visible) -> visitor.visitAnnotation(ExtractedAnnotation.edge, desc));
   }
 
   public static void writeEdge(
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 0789197..d1e20ef 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
@@ -15,12 +15,18 @@
  * annotations which overlap in name with the actual semantic AST types.
  */
 public final class AnnotationConstants {
-  public static final class Extracted {
+  public static final class ExtractedAnnotations {
     public static final String DESCRIPTOR =
         "Lcom/android/tools/r8/keepanno/annotations/ExtractedKeepAnnotations;";
+    public static final String value = "value";
+  }
+
+  public static final class ExtractedAnnotation {
+    public static final String DESCRIPTOR =
+        "Lcom/android/tools/r8/keepanno/annotations/ExtractedKeepAnnotation;";
     public static final String version = "version";
     public static final String context = "context";
-    public static final String edges = "edges";
+    public static final String edge = "edge";
   }
 
   public static final class Edge {
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java
index 9488245..51ca386 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java
@@ -20,9 +20,7 @@
 import com.android.tools.r8.keepanno.asm.KeepEdgeReader;
 import com.android.tools.r8.keepanno.asm.KeepEdgeWriter;
 import com.android.tools.r8.keepanno.ast.KeepDeclaration;
-import com.android.tools.r8.keepanno.ast.KeepEdge;
 import com.android.tools.r8.keepanno.keeprules.KeepRuleExtractorOptions;
-import com.android.tools.r8.keepanno.utils.Unimplemented;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
@@ -242,17 +240,10 @@
               null,
               "java/lang/Object",
               null);
-          for (KeepDeclaration decl : declarations) {
-            if (!decl.isKeepEdge()) {
-              throw new Unimplemented("Support check declarations...");
-            } else {
-              KeepEdge edge = decl.asKeepEdge();
-              KeepEdgeWriter.writeExtractedEdge(
-                  edge,
-                  (descriptor, visible) ->
-                      KeepAnnoTestUtils.wrap(classWriter.visitAnnotation(descriptor, visible)));
-            }
-          }
+          KeepEdgeWriter.writeExtractedEdges(
+              declarations,
+              (descriptor, visible) ->
+                  KeepAnnoTestUtils.wrap(classWriter.visitAnnotation(descriptor, visible)));
           classWriter.visitEnd();
           builder
               .getBuilder()
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepMembersApiTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepMembersApiTest.java
index fe9b59c..c6d3b7d 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepMembersApiTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepMembersApiTest.java
@@ -58,7 +58,6 @@
     assertTrue(parameters.isShrinker());
     Box<Path> lib = new Box<>();
     testForKeepAnno(parameters)
-        .skipEdgeExtraction()
         .addProgramClasses(getLibraryClasses())
         .setExcludedOuterClass(getClass())
         .applyIfShrinker(b -> lib.set(b.compile().inspect(this::checkLibraryOutput).writeToZip()));
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepTargetClassAndMemberKindTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepTargetClassAndMemberKindTest.java
index e3a9569..9afb8b1 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepTargetClassAndMemberKindTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepTargetClassAndMemberKindTest.java
@@ -36,7 +36,6 @@
   @Test
   public void test() throws Exception {
     testForKeepAnno(parameters)
-        .skipEdgeExtraction()
         .addProgramClasses(getInputClasses())
         .addKeepMainRule(TestClass.class)
         .setExcludedOuterClass(getClass())
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationTest.java
index 988e441..1c0313a 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationTest.java
@@ -35,7 +35,6 @@
   @Test
   public void test() throws Exception {
     testForKeepAnno(parameters)
-        .skipEdgeExtraction()
         .addProgramClasses(getInputClasses())
         .addKeepMainRule(TestClass.class)
         .setExcludedOuterClass(getClass())
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 977afd5..ed4a86d 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
@@ -81,6 +81,8 @@
   private static final ClassReference CHECK_OPTIMIZED_OUT = annoClass("CheckOptimizedOut");
   private static final ClassReference EXTRACTED_KEEP_ANNOTATIONS =
       annoClass("ExtractedKeepAnnotations");
+  private static final ClassReference EXTRACTED_KEEP_ANNOTATION =
+      annoClass("ExtractedKeepAnnotation");
   private static final ClassReference KEEP_EDGE = annoClass("KeepEdge");
   private static final ClassReference KEEP_BINDING = annoClass("KeepBinding");
   private static final ClassReference KEEP_TARGET = annoClass("KeepTarget");
@@ -1619,10 +1621,21 @@
     }
 
     private void generateExtractedKeepAnnotationsConstants() {
-      println("public static final class Extracted {");
+      println("public static final class ExtractedAnnotations {");
       withIndent(
           () -> {
             generateAnnotationConstants(EXTRACTED_KEEP_ANNOTATIONS);
+            new GroupMember("value")
+                .setDocTitle("Extracted normalized keep edges.")
+                .requiredArrayValue(KEEP_EDGE)
+                .generateConstants(this);
+          });
+      println("}");
+      println();
+      println("public static final class ExtractedAnnotation {");
+      withIndent(
+          () -> {
+            generateAnnotationConstants(EXTRACTED_KEEP_ANNOTATION);
             new GroupMember("version")
                 .setDocTitle("Extraction version used to generate this keep annotation.")
                 .requiredStringValue()
@@ -1631,9 +1644,9 @@
                 .setDocTitle("Extraction context from which this keep annotation is generated.")
                 .requiredStringValue()
                 .generateConstants(this);
-            new GroupMember("edges")
-                .setDocTitle("Extracted normalized keep edges.")
-                .requiredArrayValue(KEEP_EDGE)
+            new GroupMember("edge")
+                .setDocTitle("Extracted normalized keep edge.")
+                .requiredValue(KEEP_EDGE)
                 .generateConstants(this);
           });
       println("}");