[KeepAnno] Support extraction of check removed/optimized-out

Bug: b/323815449
Change-Id: I0fbf3cf1ed2def82391b10338002a06cdf730a6d
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
index 8b9d95c..e2e9461 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/ExtractedKeepAnnotation.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/ExtractedKeepAnnotation.java
@@ -27,4 +27,8 @@
 
   /** The extracted edge. */
   KeepEdge edge();
+
+  boolean isCheckRemoved() default false;
+
+  boolean isCheckOptimizedOut() default false;
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ContextDescriptor.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ContextDescriptor.java
new file mode 100644
index 0000000..66bb6d5
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ContextDescriptor.java
@@ -0,0 +1,154 @@
+// 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.asm;
+
+import static com.android.tools.r8.keepanno.ast.KeepTypePattern.fromDescriptor;
+
+import com.android.tools.r8.keepanno.ast.KeepClassItemPattern;
+import com.android.tools.r8.keepanno.ast.KeepEdgeMetaInfo;
+import com.android.tools.r8.keepanno.ast.KeepFieldNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
+import com.android.tools.r8.keepanno.ast.KeepFieldTypePattern;
+import com.android.tools.r8.keepanno.ast.KeepItemPattern;
+import com.android.tools.r8.keepanno.ast.KeepMemberItemPattern;
+import com.android.tools.r8.keepanno.ast.KeepMemberPattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodParametersPattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodPattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodReturnTypePattern;
+import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
+import com.android.tools.r8.keepanno.ast.ParsingContext;
+import org.objectweb.asm.Type;
+
+public class ContextDescriptor {
+
+  public static ContextDescriptor parse(String descriptor, ParsingContext parsingContext) {
+    int classDescriptorEnd = descriptor.indexOf(';') + 1;
+    if (classDescriptorEnd <= 0) {
+      throw parsingContext.error("Invalid descriptor: " + descriptor);
+    }
+    String classDescriptor = descriptor.substring(0, classDescriptorEnd);
+    if (classDescriptorEnd == descriptor.length()) {
+      return new ContextDescriptor(classDescriptor);
+    }
+    int memberNameEnd = descriptor.indexOf('(', classDescriptorEnd);
+    if (memberNameEnd < 0) {
+      memberNameEnd = descriptor.indexOf(':', classDescriptorEnd);
+    }
+    if (memberNameEnd < 0) {
+      throw parsingContext.error("Invalid descriptor: " + descriptor);
+    }
+    String memberName = descriptor.substring(classDescriptorEnd, memberNameEnd);
+    String memberDescriptor = descriptor.substring(memberNameEnd);
+    return new ContextDescriptor(classDescriptor, memberName, memberDescriptor);
+  }
+
+  private final String classDescriptor;
+  private final String memberName;
+  private final String memberDescriptor;
+
+  private ContextDescriptor(String classDescriptor) {
+    this(classDescriptor, null, null);
+  }
+
+  private ContextDescriptor(String classDescriptor, String memberName, String memberDescriptor) {
+    this.classDescriptor = classDescriptor;
+    this.memberName = memberName;
+    this.memberDescriptor = memberDescriptor;
+  }
+
+  public boolean isClassContext() {
+    return memberDescriptor == null;
+  }
+
+  public boolean isMethodContext() {
+    return memberDescriptor != null && memberDescriptor.charAt(0) == '(';
+  }
+
+  public boolean isFieldContext() {
+    return memberDescriptor != null && memberDescriptor.charAt(0) == ':';
+  }
+
+  public String getClassDescriptor() {
+    return classDescriptor;
+  }
+
+  public String getMemberName() {
+    return memberName;
+  }
+
+  public String getMethodDescriptor() {
+    assert isMethodContext();
+    return memberDescriptor;
+  }
+
+  public String getFieldType() {
+    assert isFieldContext();
+    return memberDescriptor.substring(1);
+  }
+
+  public KeepEdgeMetaInfo.Builder applyToMetadata(KeepEdgeMetaInfo.Builder metaInfoBuilder) {
+    if (isClassContext()) {
+      metaInfoBuilder.setContextFromClassDescriptor(getClassDescriptor());
+    } else if (isMethodContext()) {
+      metaInfoBuilder.setContextFromMethodDescriptor(
+          getClassDescriptor(), getMemberName(), getMethodDescriptor());
+    } else {
+      assert isFieldContext();
+      metaInfoBuilder.setContextFromFieldDescriptor(
+          getClassDescriptor(), getMemberName(), getFieldType());
+    }
+    return metaInfoBuilder;
+  }
+
+  public KeepItemPattern toItemPattern() {
+    KeepQualifiedClassNamePattern className =
+        KeepQualifiedClassNamePattern.exactFromDescriptor(getClassDescriptor());
+    KeepClassItemPattern classItem =
+        KeepClassItemPattern.builder().setClassNamePattern(className).build();
+    if (isClassContext()) {
+      return classItem;
+    }
+    KeepMemberPattern memberPattern;
+    if (isMethodContext()) {
+      memberPattern = createMethodPatternFromDescriptor();
+    } else {
+      assert isFieldContext();
+      memberPattern = createFieldPatternFromDescriptor();
+    }
+    return KeepMemberItemPattern.builder()
+        .setClassReference(classItem.toClassItemReference())
+        .setMemberPattern(memberPattern)
+        .build();
+  }
+
+  private KeepFieldPattern createFieldPatternFromDescriptor() {
+    return KeepFieldPattern.builder()
+        .setNamePattern(KeepFieldNamePattern.exact(getMemberName()))
+        .setTypePattern(KeepFieldTypePattern.fromType(fromDescriptor(getFieldType())))
+        .build();
+  }
+
+  private KeepMethodPattern createMethodPatternFromDescriptor() {
+    Type methodType = Type.getMethodType(getMethodDescriptor());
+
+    String returnTypeDescriptor = methodType.getReturnType().getDescriptor();
+    KeepMethodReturnTypePattern returnType =
+        returnTypeDescriptor.equals("V")
+            ? KeepMethodReturnTypePattern.voidType()
+            : KeepMethodReturnTypePattern.fromType(fromDescriptor(returnTypeDescriptor));
+
+    KeepMethodParametersPattern.Builder paramBuilder = KeepMethodParametersPattern.builder();
+    for (Type argumentType : methodType.getArgumentTypes()) {
+      paramBuilder.addParameterTypePattern(fromDescriptor(argumentType.getDescriptor()));
+    }
+
+    return KeepMethodPattern.builder()
+        .setNamePattern(KeepMethodNamePattern.exact(getMemberName()))
+        .setReturnTypePattern(returnType)
+        .setParametersPattern(paramBuilder.build())
+        .build();
+  }
+}
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 687f705..01d57c7 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
@@ -572,9 +572,11 @@
   private static class ExtractedAnnotationVisitor extends AnnotationVisitorBase {
 
     private final Parent<KeepDeclaration> parent;
-    private String context = null;
+    private ContextDescriptor context = null;
     private String version = null;
     private KeepEdgeVisitor edgeVisitor = null;
+    private boolean isCheckRemoved = false;
+    private boolean isCheckOptimizedOut = false;
 
     public ExtractedAnnotationVisitor(
         AnnotationParsingContext parsingContext, Parent<KeepDeclaration> parent) {
@@ -594,9 +596,18 @@
         version = (String) value;
         return;
       }
-      ensureVersion(getParsingContext().property(name));
+      ParsingContext parsingContext = getParsingContext().property(name);
+      ensureVersion(parsingContext);
       if (name.equals(ExtractedAnnotation.context) && value instanceof String) {
-        context = (String) value;
+        context = ContextDescriptor.parse((String) value, parsingContext);
+        return;
+      }
+      if (name.equals(ExtractedAnnotation.checkRemoved) && value instanceof Boolean) {
+        isCheckRemoved = true;
+        return;
+      }
+      if (name.equals(ExtractedAnnotation.checkOptimizedOut) && value instanceof Boolean) {
+        isCheckOptimizedOut = true;
         return;
       }
       super.visit(name, value);
@@ -616,23 +627,42 @@
     @Override
     public void visitEnd() {
       if (version == null) {
-        throw new KeepEdgeException("Invalid extracted annotation, expected a version property.");
+        throw getParsingContext()
+            .error("Invalid extracted annotation, expected a version property.");
       }
       if (context == null) {
-        throw new KeepEdgeException("Invalid extracted annotation, expected a context property.");
+        throw getParsingContext()
+            .error("Invalid extracted annotation, expected a context property.");
       }
-      if (edgeVisitor == null) {
-        throw new KeepEdgeException("Invalid extracted annotation, expected an edge property.");
+      if (edgeVisitor != null) {
+        if (isCheckRemoved || isCheckOptimizedOut) {
+          throw getParsingContext()
+              .error("Invalid extracted annotation, cannot be both an edge and check.");
+        }
+        parent.accept(
+            edgeVisitor
+                .builder
+                .setMetaInfo(context.applyToMetadata(edgeVisitor.metaInfoBuilder).build())
+                .build());
+        return;
       }
+      if (isCheckRemoved && isCheckOptimizedOut) {
+        throw getParsingContext()
+            .error(
+                "Invalid extracted annotation, cannot be both a removed and optimized-out check.");
+      }
+      if (!isCheckRemoved && !isCheckOptimizedOut) {
+        throw getParsingContext()
+            .error(
+                "Invalid extracted annotation, must specify either an edge, a removed check, or an"
+                    + " optimized-out check.");
+      }
+      KeepCheckKind kind = isCheckRemoved ? KeepCheckKind.REMOVED : KeepCheckKind.OPTIMIZED_OUT;
       parent.accept(
-          edgeVisitor
-              .builder
-              .setMetaInfo(
-                  edgeVisitor
-                      .metaInfoBuilder
-                      // TODO(b/323815449): This may be a method or field descriptor!
-                      .setContextFromClassDescriptor(context)
-                      .build())
+          KeepCheck.builder()
+              .setMetaInfo(context.applyToMetadata(KeepEdgeMetaInfo.builder()).build())
+              .setKind(kind)
+              .setItemPattern(context.toItemPattern())
               .build());
       super.visitEnd();
     }
@@ -2211,4 +2241,5 @@
       }
     }
   }
+
 }
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 560b14c..a0a98c7 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
@@ -32,6 +32,7 @@
 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.KeepEdgeException;
 import com.android.tools.r8.keepanno.ast.KeepEdgeMetaInfo;
 import com.android.tools.r8.keepanno.ast.KeepFieldAccessPattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
@@ -145,27 +146,29 @@
   }
 
   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(
-      KeepEdge edge, BiFunction<String, Boolean, AnnotationVisitorInterface> getVisitor) {
-    writeEdgeInternal(edge, (descriptor, visible) -> wrap(getVisitor.apply(descriptor, visible)));
-  }
-
-  public static void writeEdgeInternal(
-      KeepEdge edge, BiFunction<String, Boolean, AnnotationVisitor> getVisitor) {
-    withNewVisitor(
-        getVisitor.apply(Edge.DESCRIPTOR, false),
-        visitor -> new KeepEdgeWriter().writeEdge(edge, visitor));
+    decl.match(
+        edge -> {
+          withNewVisitor(
+              visitor.visitAnnotation(ExtractedAnnotation.edge, Edge.DESCRIPTOR),
+              v -> new KeepEdgeWriter().writeEdge(edge, v));
+          return null;
+        },
+        check -> {
+          switch (check.getKind()) {
+            case REMOVED:
+              visitor.visit(ExtractedAnnotation.checkRemoved, true);
+              break;
+            case OPTIMIZED_OUT:
+              visitor.visit(ExtractedAnnotation.checkOptimizedOut, true);
+              break;
+            default:
+              throw new KeepEdgeException("Unexpected keep check kind: " + check.getKind());
+          }
+          return null;
+        });
   }
 
   private void writeEdge(KeepEdge edge, AnnotationVisitor visitor) {
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 8f539a7..81ccbc6 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
@@ -26,7 +26,10 @@
         "Lcom/android/tools/r8/keepanno/annotations/ExtractedKeepAnnotation;";
     public static final String version = "version";
     public static final String context = "context";
+    public static final String keepAnnotationGroup = "keep-annotation";
     public static final String edge = "edge";
+    public static final String checkRemoved = "checkRemoved";
+    public static final String checkOptimizedOut = "checkOptimizedOut";
   }
 
   public static final class Edge {
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepDeclaration.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepDeclaration.java
index c6f6248..4425317 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepDeclaration.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepDeclaration.java
@@ -3,11 +3,20 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.keepanno.ast;
 
+import java.util.function.Function;
+
 /** Base class for the declarations represented in the keep annoations library. */
 public abstract class KeepDeclaration {
 
   public abstract KeepEdgeMetaInfo getMetaInfo();
 
+  public final <T> T match(Function<KeepEdge, T> onEdge, Function<KeepCheck, T> onCheck) {
+    if (isKeepEdge()) {
+      return onEdge.apply(asKeepEdge());
+    }
+    return onCheck.apply(asKeepCheck());
+  }
+
   public final boolean isKeepEdge() {
     return asKeepEdge() != null;
   }
diff --git a/src/test/java/com/android/tools/r8/keepanno/CheckOptimizedOutAnnotationTest.java b/src/test/java/com/android/tools/r8/keepanno/CheckOptimizedOutAnnotationTest.java
index cfe7d75..59915bc9 100644
--- a/src/test/java/com/android/tools/r8/keepanno/CheckOptimizedOutAnnotationTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/CheckOptimizedOutAnnotationTest.java
@@ -57,7 +57,6 @@
   public void testR8Native() throws Throwable {
     assumeTrue(parameters.isR8() && parameters.isNative());
     testForKeepAnno(parameters)
-        .skipEdgeExtraction()
         .addProgramClasses(getInputClasses())
         .addKeepMainRule(TestClass.class)
         .applyIfR8Native(
diff --git a/src/test/java/com/android/tools/r8/keepanno/CheckRemovedAnnotationTest.java b/src/test/java/com/android/tools/r8/keepanno/CheckRemovedAnnotationTest.java
index a1a4a2c..b3bedf1 100644
--- a/src/test/java/com/android/tools/r8/keepanno/CheckRemovedAnnotationTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/CheckRemovedAnnotationTest.java
@@ -56,7 +56,6 @@
   public void testR8Native() throws Exception {
     assumeTrue(parameters.isR8() && parameters.isNative());
     testForKeepAnno(parameters)
-        .skipEdgeExtraction()
         .addProgramClasses(getInputClasses())
         .addKeepMainRule(TestClass.class)
         .applyIfR8Native(
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 ee005f6..325960e 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
@@ -1728,9 +1728,19 @@
                 .setDocTitle("Extraction context from which this keep annotation is generated.")
                 .requiredStringValue()
                 .generateConstants(this);
-            new GroupMember("edge")
-                .setDocTitle("Extracted normalized keep edge.")
-                .requiredValue(KEEP_EDGE)
+            new Group("keep-annotation")
+                .addMember(
+                    new GroupMember("edge")
+                        .setDocTitle("Extracted normalized keep edge.")
+                        .requiredValue(KEEP_EDGE))
+                .addMember(
+                    new GroupMember("checkRemoved")
+                        .setDocTitle("Extracted check removed.")
+                        .defaultBooleanValue(false))
+                .addMember(
+                    new GroupMember("checkOptimizedOut")
+                        .setDocTitle("Extracted check optimized out.")
+                        .defaultBooleanValue(false))
                 .generateConstants(this);
           });
       println("}");