[KeepAnno] Support patterns on classes annotated by annotations

Bug: b/248408342
Change-Id: Icf922d12e77d22cd9f7a9836438a9a328862cd07
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java
index 177990e..408abf2 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java
@@ -81,6 +81,9 @@
    *   <li>instanceOfClassConstantExclusive
    *   <li>extendsClassName
    *   <li>extendsClassConstant
+   *   <li>classAnnotatedByClassName
+   *   <li>classAnnotatedByClassConstant
+   *   <li>classAnnotatedByClassNamePattern
    * </ul>
    *
    * <p>If none are specified the default is to match any class.
@@ -277,6 +280,60 @@
   Class<?> extendsClassConstant() default Object.class;
 
   /**
+   * Define the class-annotated-by pattern by fully qualified class name.
+   *
+   * <p>Mutually exclusive with the following other properties defining class-annotated-by:
+   *
+   * <ul>
+   *   <li>classAnnotatedByClassConstant
+   *   <li>classAnnotatedByClassNamePattern
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class regardless of what the class is
+   * annotated by.
+   *
+   * @return The qualified class name that defines the annotation.
+   */
+  String classAnnotatedByClassName() default "";
+
+  /**
+   * Define the class-annotated-by pattern by reference to a Class constant.
+   *
+   * <p>Mutually exclusive with the following other properties defining class-annotated-by:
+   *
+   * <ul>
+   *   <li>classAnnotatedByClassName
+   *   <li>classAnnotatedByClassNamePattern
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class regardless of what the class is
+   * annotated by.
+   *
+   * @return The class-constant that defines the annotation.
+   */
+  Class<?> classAnnotatedByClassConstant() default Object.class;
+
+  /**
+   * Define the class-annotated-by pattern by reference to a class-name pattern.
+   *
+   * <p>Mutually exclusive with the following other properties defining class-annotated-by:
+   *
+   * <ul>
+   *   <li>classAnnotatedByClassName
+   *   <li>classAnnotatedByClassConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class regardless of what the class is
+   * annotated by.
+   *
+   * @return The class-name pattern that defines the annotation.
+   */
+  ClassNamePattern classAnnotatedByClassNamePattern() default @ClassNamePattern(simpleName = "");
+
+  /**
    * Define the member-access pattern by matching on access flags.
    *
    * <p>Mutually exclusive with all field and method properties as use restricts the match to both
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java
index a35d08b..5737edf 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java
@@ -43,6 +43,9 @@
    *   <li>instanceOfClassConstantExclusive
    *   <li>extendsClassName
    *   <li>extendsClassConstant
+   *   <li>classAnnotatedByClassName
+   *   <li>classAnnotatedByClassConstant
+   *   <li>classAnnotatedByClassNamePattern
    * </ul>
    *
    * <p>If none are specified the default is to match any class.
@@ -239,6 +242,60 @@
   Class<?> extendsClassConstant() default Object.class;
 
   /**
+   * Define the class-annotated-by pattern by fully qualified class name.
+   *
+   * <p>Mutually exclusive with the following other properties defining class-annotated-by:
+   *
+   * <ul>
+   *   <li>classAnnotatedByClassConstant
+   *   <li>classAnnotatedByClassNamePattern
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class regardless of what the class is
+   * annotated by.
+   *
+   * @return The qualified class name that defines the annotation.
+   */
+  String classAnnotatedByClassName() default "";
+
+  /**
+   * Define the class-annotated-by pattern by reference to a Class constant.
+   *
+   * <p>Mutually exclusive with the following other properties defining class-annotated-by:
+   *
+   * <ul>
+   *   <li>classAnnotatedByClassName
+   *   <li>classAnnotatedByClassNamePattern
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class regardless of what the class is
+   * annotated by.
+   *
+   * @return The class-constant that defines the annotation.
+   */
+  Class<?> classAnnotatedByClassConstant() default Object.class;
+
+  /**
+   * Define the class-annotated-by pattern by reference to a class-name pattern.
+   *
+   * <p>Mutually exclusive with the following other properties defining class-annotated-by:
+   *
+   * <ul>
+   *   <li>classAnnotatedByClassName
+   *   <li>classAnnotatedByClassConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class regardless of what the class is
+   * annotated by.
+   *
+   * @return The class-name pattern that defines the annotation.
+   */
+  ClassNamePattern classAnnotatedByClassNamePattern() default @ClassNamePattern(simpleName = "");
+
+  /**
    * Define the member pattern in full by a reference to a binding.
    *
    * <p>Mutually exclusive with all other class and member pattern properties. When a member binding
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
index e2ebc3b..ed5e0ce 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
@@ -140,6 +140,9 @@
    *   <li>instanceOfClassConstantExclusive
    *   <li>extendsClassName
    *   <li>extendsClassConstant
+   *   <li>classAnnotatedByClassName
+   *   <li>classAnnotatedByClassConstant
+   *   <li>classAnnotatedByClassNamePattern
    * </ul>
    *
    * <p>If none are specified the default is to match any class.
@@ -336,6 +339,60 @@
   Class<?> extendsClassConstant() default Object.class;
 
   /**
+   * Define the class-annotated-by pattern by fully qualified class name.
+   *
+   * <p>Mutually exclusive with the following other properties defining class-annotated-by:
+   *
+   * <ul>
+   *   <li>classAnnotatedByClassConstant
+   *   <li>classAnnotatedByClassNamePattern
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class regardless of what the class is
+   * annotated by.
+   *
+   * @return The qualified class name that defines the annotation.
+   */
+  String classAnnotatedByClassName() default "";
+
+  /**
+   * Define the class-annotated-by pattern by reference to a Class constant.
+   *
+   * <p>Mutually exclusive with the following other properties defining class-annotated-by:
+   *
+   * <ul>
+   *   <li>classAnnotatedByClassName
+   *   <li>classAnnotatedByClassNamePattern
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class regardless of what the class is
+   * annotated by.
+   *
+   * @return The class-constant that defines the annotation.
+   */
+  Class<?> classAnnotatedByClassConstant() default Object.class;
+
+  /**
+   * Define the class-annotated-by pattern by reference to a class-name pattern.
+   *
+   * <p>Mutually exclusive with the following other properties defining class-annotated-by:
+   *
+   * <ul>
+   *   <li>classAnnotatedByClassName
+   *   <li>classAnnotatedByClassConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class regardless of what the class is
+   * annotated by.
+   *
+   * @return The class-name pattern that defines the annotation.
+   */
+  ClassNamePattern classAnnotatedByClassNamePattern() default @ClassNamePattern(simpleName = "");
+
+  /**
    * Define the member pattern in full by a reference to a binding.
    *
    * <p>Mutually exclusive with all other class and member pattern properties. When a member binding
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 91751e6..f25e60d 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
@@ -1085,7 +1085,8 @@
     @Override
     public AnnotationVisitor visitAnnotation(String name, String descriptor) {
       if (descriptor.equals(Target.DESCRIPTOR)) {
-        return KeepTargetVisitor.create(parsingContext, builder::addTarget, bindingsHelper);
+        return KeepTargetVisitor.create(
+            parsingContext.annotation(descriptor), builder::addTarget, bindingsHelper);
       }
       return super.visitAnnotation(name, descriptor);
     }
@@ -1403,6 +1404,7 @@
 
     private KeepClassItemReference boundClassItemReference = null;
     private final ClassNameParser classNameParser;
+    private final ClassNameParser annotatedByParser;
     private final InstanceOfDeclaration instanceOfDeclaration;
     private final List<Declaration<?>> declarations;
     private final List<PropertyParser<?, ?>> parsers;
@@ -1415,7 +1417,14 @@
       classNameParser.setProperty(Item.className, ClassNameProperty.NAME);
       classNameParser.setProperty(Item.classConstant, ClassNameProperty.CONSTANT);
       classNameParser.setProperty(Item.classNamePattern, ClassNameProperty.PATTERN);
-      parsers = ImmutableList.of(classNameParser);
+
+      annotatedByParser = new ClassNameParser(parsingContext.group(Item.classAnnotatedByGroup));
+      annotatedByParser.setProperty(Item.classAnnotatedByClassName, ClassNameProperty.NAME);
+      annotatedByParser.setProperty(Item.classAnnotatedByClassConstant, ClassNameProperty.CONSTANT);
+      annotatedByParser.setProperty(
+          Item.classAnnotatedByClassNamePattern, ClassNameProperty.PATTERN);
+
+      parsers = ImmutableList.of(classNameParser, annotatedByParser);
 
       instanceOfDeclaration = new InstanceOfDeclaration(parsingContext);
       declarations = ImmutableList.of(instanceOfDeclaration);
@@ -1435,12 +1444,14 @@
       return boundClassItemReference != null;
     }
 
-    private boolean classPatternsAreDefined() {
-      return !classNameParser.isDefault() || !instanceOfDeclaration.isDefault();
+    private boolean classPatternsAreDeclared() {
+      return classNameParser.isDeclared()
+          || annotatedByParser.isDeclared()
+          || !instanceOfDeclaration.isDefault();
     }
 
     private void checkAllowedDefinitions() {
-      if (isBindingReferenceDefined() && classPatternsAreDefined()) {
+      if (isBindingReferenceDefined() && classPatternsAreDeclared()) {
         throw parsingContext.error(
             "Cannot reference a class binding and class patterns for a single class item");
       }
@@ -1456,10 +1467,12 @@
       if (isBindingReferenceDefined()) {
         return boundClassItemReference;
       }
-      if (classPatternsAreDefined()) {
+      if (classPatternsAreDeclared()) {
         return KeepClassItemPattern.builder()
             .setClassNamePattern(
                 classNameParser.getValueOrDefault(KeepQualifiedClassNamePattern.any()))
+            .setAnnotatedByPattern(
+                annotatedByParser.getValueOrDefault(KeepQualifiedClassNamePattern.any()))
             .setInstanceOfPattern(instanceOfDeclaration.getValue())
             .build()
             .toClassItemReference();
@@ -2016,14 +2029,14 @@
     private final KeepTarget.Builder builder = KeepTarget.builder();
 
     static KeepTargetVisitor create(
-        ParsingContext parsingContext,
+        AnnotationParsingContext parsingContext,
         Parent<KeepTarget> parent,
         UserBindingsHelper bindingsHelper) {
       return new KeepTargetVisitor(parsingContext, parent, bindingsHelper);
     }
 
     private KeepTargetVisitor(
-        ParsingContext parsingContext,
+        AnnotationParsingContext parsingContext,
         Parent<KeepTarget> parent,
         UserBindingsHelper bindingsHelper) {
       super(parsingContext);
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 f0bdccf..40010dc 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
@@ -81,6 +81,11 @@
         "instanceOfClassConstantExclusive";
     public static final String extendsClassName = "extendsClassName";
     public static final String extendsClassConstant = "extendsClassConstant";
+    public static final String classAnnotatedByGroup = "class-annotated-by";
+    public static final String classAnnotatedByClassName = "classAnnotatedByClassName";
+    public static final String classAnnotatedByClassConstant = "classAnnotatedByClassConstant";
+    public static final String classAnnotatedByClassNamePattern =
+        "classAnnotatedByClassNamePattern";
     public static final String memberAccess = "memberAccess";
     public static final String methodAccess = "methodAccess";
     public static final String methodNameGroup = "method-name";
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 5c63faf..4736f07 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,12 +22,14 @@
 
     private KeepQualifiedClassNamePattern classNamePattern = KeepQualifiedClassNamePattern.any();
     private KeepInstanceOfPattern instanceOfPattern = KeepInstanceOfPattern.any();
+    private KeepQualifiedClassNamePattern annotatedByPattern = KeepQualifiedClassNamePattern.any();
 
     private Builder() {}
 
     public Builder copyFrom(KeepClassItemPattern pattern) {
       return setClassNamePattern(pattern.getClassNamePattern())
-          .setInstanceOfPattern(pattern.getInstanceOfPattern());
+          .setInstanceOfPattern(pattern.getInstanceOfPattern())
+          .setAnnotatedByPattern(pattern.getAnnotatedByPattern());
     }
 
     public Builder setClassNamePattern(KeepQualifiedClassNamePattern classNamePattern) {
@@ -40,20 +42,30 @@
       return this;
     }
 
+    public Builder setAnnotatedByPattern(KeepQualifiedClassNamePattern annotatedByPattern) {
+      this.annotatedByPattern = annotatedByPattern;
+      return this;
+    }
+
     public KeepClassItemPattern build() {
-      return new KeepClassItemPattern(classNamePattern, instanceOfPattern);
+      return new KeepClassItemPattern(classNamePattern, instanceOfPattern, annotatedByPattern);
     }
   }
 
   private final KeepQualifiedClassNamePattern classNamePattern;
   private final KeepInstanceOfPattern instanceOfPattern;
+  private final KeepQualifiedClassNamePattern annotatedByPattern;
 
   public KeepClassItemPattern(
-      KeepQualifiedClassNamePattern classNamePattern, KeepInstanceOfPattern instanceOfPattern) {
+      KeepQualifiedClassNamePattern classNamePattern,
+      KeepInstanceOfPattern instanceOfPattern,
+      KeepQualifiedClassNamePattern annotatedByPattern) {
     assert classNamePattern != null;
     assert instanceOfPattern != null;
+    assert annotatedByPattern != null;
     this.classNamePattern = classNamePattern;
     this.instanceOfPattern = instanceOfPattern;
+    this.annotatedByPattern = annotatedByPattern;
   }
 
   @Override
@@ -83,8 +95,12 @@
     return instanceOfPattern;
   }
 
+  public KeepQualifiedClassNamePattern getAnnotatedByPattern() {
+    return annotatedByPattern;
+  }
+
   public boolean isAny() {
-    return classNamePattern.isAny() && instanceOfPattern.isAny();
+    return classNamePattern.isAny() && instanceOfPattern.isAny() && annotatedByPattern.isAny();
   }
 
   @Override
@@ -97,12 +113,13 @@
     }
     KeepClassItemPattern that = (KeepClassItemPattern) obj;
     return classNamePattern.equals(that.classNamePattern)
-        && instanceOfPattern.equals(that.instanceOfPattern);
+        && instanceOfPattern.equals(that.instanceOfPattern)
+        && annotatedByPattern.equals(that.annotatedByPattern);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(classNamePattern, instanceOfPattern);
+    return Objects.hash(classNamePattern, instanceOfPattern, annotatedByPattern);
   }
 
   @Override
@@ -110,6 +127,8 @@
     return "KeepClassItemPattern"
         + "{ class="
         + classNamePattern
+        + ", annotated-by="
+        + annotatedByPattern
         + ", instance-of="
         + instanceOfPattern
         + '}';
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 04485d7..450576c 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
@@ -41,7 +41,9 @@
  *   MEMBER_ITEM_REFERENCE ::= MEMBER_BINDING_REFERENCE | MEMBER_ITEM_PATTERN
  *
  *   ITEM_PATTERN ::= CLASS_ITEM_PATTERN | MEMBER_ITEM_PATTERN
- *   CLASS_ITEM_PATTERN ::= class QUALIFIED_CLASS_NAME_PATTERN instance-of INSTANCE_OF_PATTERN
+ *   CLASS_ITEM_PATTERN ::= class QUALIFIED_CLASS_NAME_PATTERN
+ *                              annotated-by ANNOTATED_BY_PATTERN
+ *                              instance-of INSTANCE_OF_PATTERN
  *   MEMBER_ITEM_PATTERN ::= CLASS_ITEM_REFERENCE { MEMBER_PATTERN }
  *
  *   TYPE_PATTERN
@@ -65,6 +67,8 @@
  *   INSTANCE_OF_PATTERN_INCLUSIVE ::= QUALIFIED_CLASS_NAME_PATTERN
  *   INSTANCE_OF_PATTERN_EXCLUSIVE ::= QUALIFIED_CLASS_NAME_PATTERN
  *
+ *   ANNOTATED_BY_PATTERN ::= QUALIFIED_CLASS_NAME_PATTERN
+ *
  *   MEMBER_PATTERN ::= none | all | FIELD_PATTERN | METHOD_PATTERN
  *
  *   FIELD_PATTERN
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 7737744..4f2ee93 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
@@ -98,6 +98,12 @@
       StringBuilder builder,
       KeepClassItemPattern classPattern,
       BiConsumer<StringBuilder, KeepQualifiedClassNamePattern> printClassName) {
+    KeepQualifiedClassNamePattern annotatedByPattern = classPattern.getAnnotatedByPattern();
+    if (!annotatedByPattern.isAny()) {
+      builder.append("@");
+      printClassName(annotatedByPattern, RulePrinter.withoutBackReferences(builder));
+      builder.append(" ");
+    }
     builder.append("class ");
     printClassName.accept(builder, classPattern.getClassNamePattern());
     KeepInstanceOfPattern extendsPattern = classPattern.getInstanceOfPattern();
diff --git a/src/test/java/com/android/tools/r8/keepanno/ClassAnnotatedByPatternsTest.java b/src/test/java/com/android/tools/r8/keepanno/ClassAnnotatedByPatternsTest.java
new file mode 100644
index 0000000..5897492
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/ClassAnnotatedByPatternsTest.java
@@ -0,0 +1,128 @@
+// 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.isAbsent;
+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.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 ClassAnnotatedByPatternsTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("C1", "C2");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public ClassAnnotatedByPatternsTest(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()
+        .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 annotation remains.
+    assertThat(inspector.clazz(A1.class), isPresentAndRenamed());
+    // Nothing is using or keeping the second annotation.
+    assertThat(inspector.clazz(A2.class), isAbsent());
+    // The first two classes are annotated by A1 so the keep-annotation applies and must retain
+    // 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(
+            classAnnotatedByClassConstant = A1.class,
+            constraints = {KeepConstraint.ANNOTATIONS, KeepConstraint.NAME}))
+    public void foo(Class<?>... classes) throws Exception {
+      for (Class<?> clazz : classes) {
+        if (clazz.isAnnotationPresent(A1.class)) {
+          String typeName = clazz.getTypeName();
+          System.out.println(typeName.substring(typeName.lastIndexOf('$') + 1));
+        }
+      }
+    }
+  }
+
+  @A1
+  static class C1 {}
+
+  @A1
+  @A2
+  static class C2 {}
+
+  @A2
+  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);
+    }
+  }
+}
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 61265cb..4b7c0c4 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
@@ -297,6 +297,7 @@
     private static String CLASS_GROUP = "class";
     private static String CLASS_NAME_GROUP = "class-name";
     private static String INSTANCE_OF_GROUP = "instance-of";
+    private static String CLASS_ANNOTATED_BY_GROUP = "class-annotated-by";
 
     private Group createDescriptionGroup() {
       return new Group("description")
@@ -681,6 +682,44 @@
               "If none are specified the default is to match any class instance.");
     }
 
+    private GroupMember classAnnotatedByClassName() {
+      return new GroupMember("classAnnotatedByClassName")
+          .setDocTitle(
+              "Define the " + CLASS_ANNOTATED_BY_GROUP + " pattern by fully qualified class name.")
+          .setDocReturn("The qualified class name that defines the annotation.")
+          .defaultEmptyString();
+    }
+
+    private GroupMember classAnnotatedByClassConstant() {
+      return new GroupMember("classAnnotatedByClassConstant")
+          .setDocTitle(
+              "Define the "
+                  + CLASS_ANNOTATED_BY_GROUP
+                  + " pattern by reference to a Class constant.")
+          .setDocReturn("The class-constant that defines the annotation.")
+          .defaultObjectClass();
+    }
+
+    private GroupMember classAnnotatedByClassNamePattern() {
+      return new GroupMember("classAnnotatedByClassNamePattern")
+          .setDocTitle(
+              "Define the "
+                  + CLASS_ANNOTATED_BY_GROUP
+                  + " pattern by reference to a class-name pattern.")
+          .setDocReturn("The class-name pattern that defines the annotation.")
+          .defaultValue(ClassNamePattern.class, DEFAULT_INVALID_CLASS_NAME_PATTERN);
+    }
+
+    private Group createClassAnnotatedByPatternGroup() {
+      return new Group(CLASS_ANNOTATED_BY_GROUP)
+          .addMember(classAnnotatedByClassName())
+          .addMember(classAnnotatedByClassConstant())
+          .addMember(classAnnotatedByClassNamePattern())
+          .addDocFooterParagraph(
+              "If none are specified the default is to match any class "
+                  + "regardless of what the class is annotated by.");
+    }
+
     private Group createMemberBindingGroup() {
       return new Group("member")
           .allowMutuallyExclusiveWithOtherGroups()
@@ -870,7 +909,9 @@
         Group bindingGroup = createClassBindingGroup();
         Group classNameGroup = createClassNamePatternGroup();
         Group classInstanceOfGroup = createClassInstanceOfPatternGroup();
-        bindingGroup.addMutuallyExclusiveGroups(classNameGroup, classInstanceOfGroup);
+        Group classAnnotatedByGroup = createClassAnnotatedByPatternGroup();
+        bindingGroup.addMutuallyExclusiveGroups(
+            classNameGroup, classInstanceOfGroup, classAnnotatedByGroup);
 
         bindingGroup.generate(this);
         println();
@@ -878,6 +919,8 @@
         println();
         classInstanceOfGroup.generate(this);
         println();
+        classAnnotatedByGroup.generate(this);
+        println();
       }
 
       // Member binding properties.
@@ -1434,6 +1477,7 @@
             // Classes.
             createClassNamePatternGroup().generateConstants(this);
             createClassInstanceOfPatternGroup().generateConstants(this);
+            createClassAnnotatedByPatternGroup().generateConstants(this);
             // Members.
             createMemberAccessGroup().generateConstants(this);
             // Methods.