[KeepAnno] Distinguish unconstrained and any-actual annotated-by pattern

Bug: b/248408342
Change-Id: I65ca8cd3447e9ac25c6867a6ff0da7342c8b6e5b
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 7f0d34d..fc4cd2e 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
@@ -56,6 +56,7 @@
 import com.android.tools.r8.keepanno.ast.KeepStringPattern;
 import com.android.tools.r8.keepanno.ast.KeepTarget;
 import com.android.tools.r8.keepanno.ast.KeepTypePattern;
+import com.android.tools.r8.keepanno.ast.OptionalPattern;
 import com.android.tools.r8.keepanno.ast.ParsingContext;
 import com.android.tools.r8.keepanno.ast.ParsingContext.AnnotationParsingContext;
 import com.android.tools.r8.keepanno.ast.ParsingContext.ClassParsingContext;
@@ -1331,8 +1332,7 @@
         return KeepClassItemPattern.builder()
             .setClassNamePattern(
                 classNameParser.getValueOrDefault(KeepQualifiedClassNamePattern.any()))
-            .setAnnotatedByPattern(
-                annotatedByParser.getValueOrDefault(KeepQualifiedClassNamePattern.any()))
+            .setAnnotatedByPattern(OptionalPattern.ofNullable(annotatedByParser.getValue()))
             .setInstanceOfPattern(instanceOfParser.getValueOrDefault(KeepInstanceOfPattern.any()))
             .build()
             .toClassItemReference();
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassItemPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassItemPattern.java
index 4736f07..2ecadbc 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassItemPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassItemPattern.java
@@ -22,7 +22,8 @@
 
     private KeepQualifiedClassNamePattern classNamePattern = KeepQualifiedClassNamePattern.any();
     private KeepInstanceOfPattern instanceOfPattern = KeepInstanceOfPattern.any();
-    private KeepQualifiedClassNamePattern annotatedByPattern = KeepQualifiedClassNamePattern.any();
+    private OptionalPattern<KeepQualifiedClassNamePattern> annotatedByPattern =
+        OptionalPattern.absent();
 
     private Builder() {}
 
@@ -42,7 +43,9 @@
       return this;
     }
 
-    public Builder setAnnotatedByPattern(KeepQualifiedClassNamePattern annotatedByPattern) {
+    public Builder setAnnotatedByPattern(
+        OptionalPattern<KeepQualifiedClassNamePattern> annotatedByPattern) {
+      assert annotatedByPattern != null;
       this.annotatedByPattern = annotatedByPattern;
       return this;
     }
@@ -54,12 +57,12 @@
 
   private final KeepQualifiedClassNamePattern classNamePattern;
   private final KeepInstanceOfPattern instanceOfPattern;
-  private final KeepQualifiedClassNamePattern annotatedByPattern;
+  private final OptionalPattern<KeepQualifiedClassNamePattern> annotatedByPattern;
 
   public KeepClassItemPattern(
       KeepQualifiedClassNamePattern classNamePattern,
       KeepInstanceOfPattern instanceOfPattern,
-      KeepQualifiedClassNamePattern annotatedByPattern) {
+      OptionalPattern<KeepQualifiedClassNamePattern> annotatedByPattern) {
     assert classNamePattern != null;
     assert instanceOfPattern != null;
     assert annotatedByPattern != null;
@@ -95,12 +98,12 @@
     return instanceOfPattern;
   }
 
-  public KeepQualifiedClassNamePattern getAnnotatedByPattern() {
+  public OptionalPattern<KeepQualifiedClassNamePattern> getAnnotatedByPattern() {
     return annotatedByPattern;
   }
 
   public boolean isAny() {
-    return classNamePattern.isAny() && instanceOfPattern.isAny() && annotatedByPattern.isAny();
+    return classNamePattern.isAny() && instanceOfPattern.isAny() && annotatedByPattern.isAbsent();
   }
 
   @Override
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java
index 450576c..ce58000 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java
@@ -17,6 +17,8 @@
  * <p>TODO(b/248408342): Update the BNF and AST to be complete.
  *
  * <pre>
+ *   OPT(T) ::= absent | present T
+ *
  *   EDGE ::= METAINFO BINDINGS PRECONDITIONS -> CONSEQUENCES
  *   METAINFO ::= [CONTEXT] [DESCRIPTION]
  *   CONTEXT ::= class-descriptor | method-descriptor | field-descriptor
@@ -42,7 +44,7 @@
  *
  *   ITEM_PATTERN ::= CLASS_ITEM_PATTERN | MEMBER_ITEM_PATTERN
  *   CLASS_ITEM_PATTERN ::= class QUALIFIED_CLASS_NAME_PATTERN
- *                              annotated-by ANNOTATED_BY_PATTERN
+ *                              annotated-by OPT(ANNOTATED_BY_PATTERN)
  *                              instance-of INSTANCE_OF_PATTERN
  *   MEMBER_ITEM_PATTERN ::= CLASS_ITEM_REFERENCE { MEMBER_PATTERN }
  *
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/OptionalPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/OptionalPattern.java
new file mode 100644
index 0000000..18061b6
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/OptionalPattern.java
@@ -0,0 +1,96 @@
+// 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.ast;
+
+import java.util.function.Function;
+
+public abstract class OptionalPattern<T> {
+
+  public static <T> OptionalPattern<T> absent() {
+    return (OptionalPattern<T>) Absent.INSTANCE;
+  }
+
+  public static <T> OptionalPattern<T> of(T value) {
+    assert value != null;
+    return new Some<>(value);
+  }
+
+  public static <T> OptionalPattern<T> ofNullable(T value) {
+    return value != null ? of(value) : absent();
+  }
+
+  public abstract boolean isAbsent();
+
+  public final boolean isPresent() {
+    return !isAbsent();
+  }
+
+  public T get() {
+    throw new KeepEdgeException("Unexpected attempt to get absent value");
+  }
+
+  public <S> OptionalPattern<S> map(Function<T, S> fn) {
+    return absent();
+  }
+
+  private static final class Absent extends OptionalPattern {
+    private static final Absent INSTANCE = new Absent();
+
+    @Override
+    public boolean isAbsent() {
+      return true;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      return this == obj;
+    }
+
+    @Override
+    public int hashCode() {
+      return System.identityHashCode(this);
+    }
+  }
+
+  private static final class Some<T> extends OptionalPattern<T> {
+    private final T value;
+
+    public Some(T value) {
+      this.value = value;
+    }
+
+    @Override
+    public boolean isAbsent() {
+      return false;
+    }
+
+    @Override
+    public T get() {
+      return value;
+    }
+
+    @Override
+    public <S> OptionalPattern<S> map(Function<T, S> fn) {
+      return of(fn.apply(value));
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof Some)) {
+        return false;
+      }
+      Some<?> some = (Some<?>) o;
+      return value.equals(some.value);
+    }
+
+    @Override
+    public int hashCode() {
+      return value.hashCode();
+    }
+  }
+}
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 4f2ee93..3e78d23 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
@@ -28,6 +28,7 @@
 import com.android.tools.r8.keepanno.ast.KeepTypePattern;
 import com.android.tools.r8.keepanno.ast.KeepUnqualfiedClassNamePattern;
 import com.android.tools.r8.keepanno.ast.ModifierPattern;
+import com.android.tools.r8.keepanno.ast.OptionalPattern;
 import com.android.tools.r8.keepanno.utils.Unimplemented;
 import java.util.List;
 import java.util.Set;
@@ -98,10 +99,11 @@
       StringBuilder builder,
       KeepClassItemPattern classPattern,
       BiConsumer<StringBuilder, KeepQualifiedClassNamePattern> printClassName) {
-    KeepQualifiedClassNamePattern annotatedByPattern = classPattern.getAnnotatedByPattern();
-    if (!annotatedByPattern.isAny()) {
+    OptionalPattern<KeepQualifiedClassNamePattern> annotatedByPattern =
+        classPattern.getAnnotatedByPattern();
+    if (annotatedByPattern.isPresent()) {
       builder.append("@");
-      printClassName(annotatedByPattern, RulePrinter.withoutBackReferences(builder));
+      printClassName(annotatedByPattern.get(), RulePrinter.withoutBackReferences(builder));
       builder.append(" ");
     }
     builder.append("class ");
diff --git a/src/test/java/com/android/tools/r8/keepanno/ClassAnnotatedByAnyAnnoPatternTest.java b/src/test/java/com/android/tools/r8/keepanno/ClassAnnotatedByAnyAnnoPatternTest.java
new file mode 100644
index 0000000..2a265db
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/ClassAnnotatedByAnyAnnoPatternTest.java
@@ -0,0 +1,135 @@
+// 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 static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+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.ClassNamePattern;
+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.UsedByReflection;
+import com.android.tools.r8.keepanno.annotations.UsesReflection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ClassAnnotatedByAnyAnnoPatternTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("C1: A1", "C2: A2");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public ClassAnnotatedByAnyAnnoPatternTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getInputClasses())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .enableExperimentalKeepAnnotations()
+        .addProgramClasses(getInputClasses())
+        .setMinApi(parameters)
+        // TODO(b/248408342): Make this implicit when annotations are kept by the keep-annotation.
+        .addKeepRuntimeVisibleAnnotations()
+        .compile()
+        .apply(b -> System.out.println(b.getProguardConfiguration()))
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkOutput);
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(
+        TestClass.class, Reflector.class, A1.class, A2.class, C1.class, C2.class, C3.class);
+  }
+
+  private void checkOutput(CodeInspector inspector) {
+    // The class constant use will ensure the annotations remains.
+    // They are not renamed as the keep-annotation will match them (they are themselves annotated).
+    assertThat(inspector.clazz(A1.class), isPresentAndNotRenamed());
+    assertThat(inspector.clazz(A2.class), isPresentAndNotRenamed());
+    // The first two classes are annotated so the keep-annotation applies and retains their name.
+    assertThat(inspector.clazz(C1.class), isPresentAndNotRenamed());
+    assertThat(inspector.clazz(C2.class), isPresentAndNotRenamed());
+    // The last class will remain due to the class constant, but it is optimized/renamed.
+    assertThat(inspector.clazz(C3.class), isPresentAndRenamed());
+  }
+
+  @Target(ElementType.TYPE)
+  @Retention(RetentionPolicy.RUNTIME)
+  @interface A1 {}
+
+  @Target(ElementType.TYPE)
+  @Retention(RetentionPolicy.RUNTIME)
+  @interface A2 {}
+
+  static class Reflector {
+
+    @UsesReflection({
+      @KeepTarget(
+          classAnnotatedByClassNamePattern = @ClassNamePattern,
+          constraints = {KeepConstraint.ANNOTATIONS, KeepConstraint.NAME}),
+    })
+    public void foo(Class<?>... classes) throws Exception {
+      for (Class<?> clazz : classes) {
+        if (clazz.getAnnotations().length > 0) {
+          String typeName = clazz.getTypeName();
+          System.out.print(typeName.substring(typeName.lastIndexOf('$') + 1) + ":");
+          if (clazz.isAnnotationPresent(A1.class)) {
+            System.out.print(" A1");
+          }
+          if (clazz.isAnnotationPresent(A2.class)) {
+            System.out.print(" A2");
+          }
+          System.out.println();
+        }
+      }
+    }
+  }
+
+  @A1
+  static class C1 {}
+
+  @A2
+  static class C2 {}
+
+  static class C3 {}
+
+  static class TestClass {
+
+    @UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS)
+    public static void main(String[] args) throws Exception {
+      new Reflector().foo(C1.class, C2.class, C3.class);
+    }
+  }
+}