[KeepAnno] Add class-name patterns to the annotation language

The class-name patterns allow splitting the match on simple names and packages.

Bug: b/248408342
Change-Id: Ia4a736878f0ebf72e0b1443077a3f7240c83b657
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/ClassNamePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/ClassNamePattern.java
new file mode 100644
index 0000000..1d40ae5
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/ClassNamePattern.java
@@ -0,0 +1,42 @@
+// Copyright (c) 2023, 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.
+
+// ***********************************************************************************
+// GENERATED FILE. DO NOT EDIT! See KeepItemAnnotationGenerator.java.
+// ***********************************************************************************
+
+package com.android.tools.r8.keepanno.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * A pattern structure for matching names of classes and interfaces.
+ *
+ * <p>If no properties are set, the default pattern matches any name of a class or interface.
+ */
+@Target(ElementType.ANNOTATION_TYPE)
+@Retention(RetentionPolicy.CLASS)
+public @interface ClassNamePattern {
+
+  /**
+   * Exact simple name of the class or interface.
+   *
+   * <p>For example, the simple name {@code com.example.MyClass} is {@code MyClass}.
+   *
+   * <p>The default matches any simple name.
+   */
+  String simpleName() default "";
+
+  /**
+   * Exact package name of the class or interface.
+   *
+   * <p>For example, the package of {@code com.example.MyClass} is {@code com.example}.
+   *
+   * <p>The default matches any package.
+   */
+  String packageName() default "";
+}
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 579d46f..3f1152e 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
@@ -74,6 +74,7 @@
    * <ul>
    *   <li>className
    *   <li>classConstant
+   *   <li>classNamePattern
    *   <li>instanceOfClassName
    *   <li>instanceOfClassNameExclusive
    *   <li>instanceOfClassConstant
@@ -95,6 +96,7 @@
    *
    * <ul>
    *   <li>classConstant
+   *   <li>classNamePattern
    *   <li>classFromBinding
    * </ul>
    *
@@ -111,6 +113,7 @@
    *
    * <ul>
    *   <li>className
+   *   <li>classNamePattern
    *   <li>classFromBinding
    * </ul>
    *
@@ -121,6 +124,23 @@
   Class<?> classConstant() default Object.class;
 
   /**
+   * Define the class-name pattern by reference to a class-name pattern.
+   *
+   * <p>Mutually exclusive with the following other properties defining class-name:
+   *
+   * <ul>
+   *   <li>className
+   *   <li>classConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class name.
+   *
+   * @return The class-name pattern that defines the class.
+   */
+  ClassNamePattern classNamePattern() default @ClassNamePattern(simpleName = "");
+
+  /**
    * Define the instance-of pattern as classes that are instances of the fully qualified class name.
    *
    * <p>Mutually exclusive with the following other properties defining instance-of:
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 470266b..74b2add 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
@@ -36,6 +36,7 @@
    * <ul>
    *   <li>className
    *   <li>classConstant
+   *   <li>classNamePattern
    *   <li>instanceOfClassName
    *   <li>instanceOfClassNameExclusive
    *   <li>instanceOfClassConstant
@@ -57,6 +58,7 @@
    *
    * <ul>
    *   <li>classConstant
+   *   <li>classNamePattern
    *   <li>classFromBinding
    * </ul>
    *
@@ -73,6 +75,7 @@
    *
    * <ul>
    *   <li>className
+   *   <li>classNamePattern
    *   <li>classFromBinding
    * </ul>
    *
@@ -83,6 +86,23 @@
   Class<?> classConstant() default Object.class;
 
   /**
+   * Define the class-name pattern by reference to a class-name pattern.
+   *
+   * <p>Mutually exclusive with the following other properties defining class-name:
+   *
+   * <ul>
+   *   <li>className
+   *   <li>classConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class name.
+   *
+   * @return The class-name pattern that defines the class.
+   */
+  ClassNamePattern classNamePattern() default @ClassNamePattern(simpleName = "");
+
+  /**
    * Define the instance-of pattern as classes that are instances of the fully qualified class name.
    *
    * <p>Mutually exclusive with the following other properties defining instance-of:
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 7bbc2f0..d60525d 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
@@ -133,6 +133,7 @@
    * <ul>
    *   <li>className
    *   <li>classConstant
+   *   <li>classNamePattern
    *   <li>instanceOfClassName
    *   <li>instanceOfClassNameExclusive
    *   <li>instanceOfClassConstant
@@ -154,6 +155,7 @@
    *
    * <ul>
    *   <li>classConstant
+   *   <li>classNamePattern
    *   <li>classFromBinding
    * </ul>
    *
@@ -170,6 +172,7 @@
    *
    * <ul>
    *   <li>className
+   *   <li>classNamePattern
    *   <li>classFromBinding
    * </ul>
    *
@@ -180,6 +183,23 @@
   Class<?> classConstant() default Object.class;
 
   /**
+   * Define the class-name pattern by reference to a class-name pattern.
+   *
+   * <p>Mutually exclusive with the following other properties defining class-name:
+   *
+   * <ul>
+   *   <li>className
+   *   <li>classConstant
+   *   <li>classFromBinding
+   * </ul>
+   *
+   * <p>If none are specified the default is to match any class name.
+   *
+   * @return The class-name pattern that defines the class.
+   */
+  ClassNamePattern classNamePattern() default @ClassNamePattern(simpleName = "");
+
+  /**
    * Define the instance-of pattern as classes that are instances of the fully qualified class name.
    *
    * <p>Mutually exclusive with the following other properties defining instance-of:
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/TypePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/TypePattern.java
index 1d7e6c6..2fa0ffd 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/TypePattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/TypePattern.java
@@ -29,7 +29,12 @@
    *
    * <p>For example, {@code "long"} or {@code "java.lang.String"}.
    *
-   * <p>Mutually exclusive with the property `constant` also defining type-pattern.
+   * <p>Mutually exclusive with the following other properties defining type-pattern:
+   *
+   * <ul>
+   *   <li>constant
+   *   <li>classNamePattern
+   * </ul>
    */
   String name() default "";
 
@@ -38,7 +43,24 @@
    *
    * <p>For example, {@code String.class}.
    *
-   * <p>Mutually exclusive with the property `name` also defining type-pattern.
+   * <p>Mutually exclusive with the following other properties defining type-pattern:
+   *
+   * <ul>
+   *   <li>name
+   *   <li>classNamePattern
+   * </ul>
    */
   Class<?> constant() default Object.class;
+
+  /**
+   * Classes matching the class-name pattern.
+   *
+   * <p>Mutually exclusive with the following other properties defining type-pattern:
+   *
+   * <ul>
+   *   <li>name
+   *   <li>constant
+   * </ul>
+   */
+  ClassNamePattern classNamePattern() default @ClassNamePattern(simpleName = "");
 }
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 35beddf..cee27a8 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
@@ -6,6 +6,7 @@
 import com.android.tools.r8.keepanno.ast.AccessVisibility;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Binding;
+import com.android.tools.r8.keepanno.ast.AnnotationConstants.ClassNamePattern;
 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;
@@ -50,10 +51,13 @@
 import com.android.tools.r8.keepanno.ast.KeepMethodReturnTypePattern;
 import com.android.tools.r8.keepanno.ast.KeepOptions;
 import com.android.tools.r8.keepanno.ast.KeepOptions.KeepOption;
+import com.android.tools.r8.keepanno.ast.KeepPackagePattern;
 import com.android.tools.r8.keepanno.ast.KeepPreconditions;
 import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
 import com.android.tools.r8.keepanno.ast.KeepTarget;
 import com.android.tools.r8.keepanno.ast.KeepTypePattern;
+import com.android.tools.r8.keepanno.ast.KeepUnqualfiedClassNamePattern;
+import com.android.tools.r8.keepanno.utils.Unimplemented;
 import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -1195,7 +1199,6 @@
       }
       return true;
     }
-    ;
 
     abstract T getValue();
 
@@ -1341,6 +1344,15 @@
       }
       return null;
     }
+
+    @Override
+    AnnotationVisitor parseAnnotation(
+        String name, String descriptor, Consumer<KeepQualifiedClassNamePattern> setValue) {
+      if (name.equals(Item.classNamePattern) && descriptor.equals(ClassNamePattern.DESCRIPTOR)) {
+        return new ClassNamePatternVisitor(setValue);
+      }
+      return super.parseAnnotation(name, descriptor, setValue);
+    }
   }
 
   private static class InstanceOfDeclaration extends SingleDeclaration<KeepInstanceOfPattern> {
@@ -1402,11 +1414,18 @@
     private KeepClassItemReference boundClassItemReference = null;
     private final ClassNameDeclaration classNameDeclaration = new ClassNameDeclaration();
     private final InstanceOfDeclaration instanceOfDeclaration = new InstanceOfDeclaration();
+    private final List<Declaration<?>> declarations =
+        ImmutableList.of(classNameDeclaration, instanceOfDeclaration);
 
     public ClassDeclaration(Supplier<UserBindingsHelper> getBindingsHelper) {
       this.getBindingsHelper = getBindingsHelper;
     }
 
+    @Override
+    List<Declaration<?>> declarations() {
+      return declarations;
+    }
+
     private boolean isBindingReferenceDefined() {
       return boundClassItemReference != null;
     }
@@ -1429,11 +1448,12 @@
 
     @Override
     boolean isDefault() {
-      return !isBindingReferenceDefined() && !classPatternsAreDefined();
+      return !isBindingReferenceDefined() && super.isDefault();
     }
 
     @Override
     KeepClassItemReference getValue() {
+      checkAllowedDefinitions();
       if (isBindingReferenceDefined()) {
         return boundClassItemReference;
       }
@@ -1463,15 +1483,7 @@
         setBindingReference(KeepBindingReference.forClass(symbol).toClassItemReference());
         return true;
       }
-      if (classNameDeclaration.tryParse(name, value)) {
-        checkAllowedDefinitions();
-        return true;
-      }
-      if (instanceOfDeclaration.tryParse(name, value)) {
-        checkAllowedDefinitions();
-        return true;
-      }
-      return false;
+      return super.tryParse(name, value);
     }
   }
 
@@ -2139,6 +2151,109 @@
     }
   }
 
+  private static class ClassSimpleNameDeclaration
+      extends SingleDeclaration<KeepUnqualfiedClassNamePattern> {
+
+    @Override
+    String kind() {
+      return "class-simple-name";
+    }
+
+    @Override
+    KeepUnqualfiedClassNamePattern getDefaultValue() {
+      return KeepUnqualfiedClassNamePattern.any();
+    }
+
+    @Override
+    KeepUnqualfiedClassNamePattern parse(String name, Object value) {
+      if (name.equals(ClassNamePattern.simpleName) && value instanceof String) {
+        return KeepUnqualfiedClassNamePattern.builder().exact((String) value).build();
+      }
+      return null;
+    }
+  }
+
+  private static class PackageDeclaration extends SingleDeclaration<KeepPackagePattern> {
+
+    @Override
+    String kind() {
+      return "package";
+    }
+
+    @Override
+    KeepPackagePattern getDefaultValue() {
+      return KeepPackagePattern.any();
+    }
+
+    @Override
+    KeepPackagePattern parse(String name, Object value) {
+      if (name.equals(ClassNamePattern.packageName) && value instanceof String) {
+        return KeepPackagePattern.builder().exact((String) value).build();
+      }
+      return null;
+    }
+  }
+
+  private static class ClassNamePatternDeclaration
+      extends Declaration<KeepQualifiedClassNamePattern> {
+
+    private final ClassSimpleNameDeclaration nameDeclaration = new ClassSimpleNameDeclaration();
+    private final PackageDeclaration packageDeclaration = new PackageDeclaration();
+    private final List<Declaration<?>> declarations =
+        ImmutableList.of(nameDeclaration, packageDeclaration);
+
+    @Override
+    String kind() {
+      return "class-name";
+    }
+
+    @Override
+    KeepQualifiedClassNamePattern getValue() {
+      if (!packageDeclaration.isDefault() || !nameDeclaration.isDefault()) {
+        return KeepQualifiedClassNamePattern.builder()
+            .setPackagePattern(packageDeclaration.getValue())
+            .setNamePattern(nameDeclaration.getValue())
+            .build();
+      }
+      return null;
+    }
+
+    @Override
+    List<Declaration<?>> declarations() {
+      return declarations;
+    }
+  }
+
+  private static class ClassNamePatternVisitor extends AnnotationVisitorBase {
+
+    private final ClassNamePatternDeclaration declaration = new ClassNamePatternDeclaration();
+    private final Consumer<KeepQualifiedClassNamePattern> setValue;
+
+    public ClassNamePatternVisitor(Consumer<KeepQualifiedClassNamePattern> setValue) {
+      this.setValue = setValue;
+    }
+
+    @Override
+    public String getAnnotationName() {
+      return ClassNamePattern.SIMPLE_NAME;
+    }
+
+    @Override
+    public void visit(String name, Object value) {
+      if (!declaration.tryParse(name, value)) {
+        super.visit(name, value);
+      }
+    }
+
+    @Override
+    public void visitEnd() {
+      if (!declaration.isDefault()) {
+        setValue.accept(declaration.getValue());
+      }
+      super.visitEnd();
+    }
+  }
+
   private static class TypePatternVisitor extends AnnotationVisitorBase {
     private final Supplier<String> annotationName;
     private final Consumer<KeepTypePattern> consumer;
@@ -2177,6 +2292,23 @@
     }
 
     @Override
+    public AnnotationVisitor visitAnnotation(String name, String descriptor) {
+      if (TypePattern.classNamePattern.equals(name)
+          && descriptor.equals(ClassNamePattern.DESCRIPTOR)) {
+        return new ClassNamePatternVisitor(
+            p -> {
+              if (p.isExact()) {
+                setResult(KeepTypePattern.fromDescriptor(p.getExactDescriptor()));
+              } else {
+                // TODO(b/248408342): Extend the AST type patterns.
+                throw new Unimplemented("Non-exact class patterns are not unimplemented yet");
+              }
+            });
+      }
+      return super.visitAnnotation(name, descriptor);
+    }
+
+    @Override
     public void visitEnd() {
       consumer.accept(result != null ? result : KeepTypePattern.any());
     }
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 1ab1561..cae779e 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
@@ -76,6 +76,7 @@
     public static final String memberFromBinding = "memberFromBinding";
     public static final String className = "className";
     public static final String classConstant = "classConstant";
+    public static final String classNamePattern = "classNamePattern";
     public static final String instanceOfClassName = "instanceOfClassName";
     public static final String instanceOfClassNameExclusive = "instanceOfClassNameExclusive";
     public static final String instanceOfClassConstant = "instanceOfClassConstant";
@@ -203,5 +204,14 @@
         "Lcom/android/tools/r8/keepanno/annotations/TypePattern;";
     public static final String name = "name";
     public static final String constant = "constant";
+    public static final String classNamePattern = "classNamePattern";
+  }
+
+  public static final class ClassNamePattern {
+    public static final String SIMPLE_NAME = "ClassNamePattern";
+    public static final String DESCRIPTOR =
+        "Lcom/android/tools/r8/keepanno/annotations/ClassNamePattern;";
+    public static final String simpleName = "simpleName";
+    public static final String packageName = "packageName";
   }
 }
diff --git a/src/test/java/com/android/tools/r8/keepanno/classpatterns/ClassNamePatternsTest.java b/src/test/java/com/android/tools/r8/keepanno/classpatterns/ClassNamePatternsTest.java
new file mode 100644
index 0000000..3468b4c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/classpatterns/ClassNamePatternsTest.java
@@ -0,0 +1,192 @@
+// Copyright (c) 2023, 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.classpatterns;
+
+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.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.TypePattern;
+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.google.common.collect.ImmutableList;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ClassNamePatternsTest extends TestBase {
+
+  static final Class<?> A1 = com.android.tools.r8.keepanno.classpatterns.pkg1.A.class;
+  static final Class<?> B1 = com.android.tools.r8.keepanno.classpatterns.pkg1.B.class;
+  static final Class<?> A2 = com.android.tools.r8.keepanno.classpatterns.pkg2.A.class;
+  static final Class<?> B2 = com.android.tools.r8.keepanno.classpatterns.pkg2.B.class;
+
+  static final String EXPECTED_ALL = StringUtils.lines("pkg1.A", "pkg1.B", "pkg2.A", "pkg2.B");
+  static final String EXPECTED_PKG = StringUtils.lines("pkg1.A", "pkg1.B");
+  static final String EXPECTED_NAME = StringUtils.lines("pkg1.B", "pkg2.B");
+  static final String EXPECTED_SINGLE = StringUtils.lines("pkg2.A");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public ClassNamePatternsTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getBaseInputClasses())
+        .addProgramClasses(TestAll.class)
+        .run(parameters.getRuntime(), TestAll.class)
+        .assertSuccessWithOutput(EXPECTED_ALL);
+  }
+
+  private void runTestR8(Class<?> mainClass, String expected) throws Exception {
+    testForR8(parameters.getBackend())
+        .enableExperimentalKeepAnnotations()
+        .addProgramClasses(getBaseInputClasses())
+        .addProgramClasses(mainClass)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), mainClass)
+        .assertSuccessWithOutput(expected);
+  }
+
+  @Test
+  public void testAllR8() throws Exception {
+    runTestR8(TestAll.class, EXPECTED_ALL);
+  }
+
+  @Test
+  public void testPkgR8() throws Exception {
+    runTestR8(TestPkg.class, EXPECTED_PKG);
+  }
+
+  @Test
+  public void testNameR8() throws Exception {
+    runTestR8(TestName.class, EXPECTED_NAME);
+  }
+
+  @Test
+  public void testSingleR8() throws Exception {
+    runTestR8(TestSingle.class, EXPECTED_SINGLE);
+  }
+
+  public List<Class<?>> getBaseInputClasses() {
+    return ImmutableList.of(Util.class, A1, B1, A2, B2);
+  }
+
+  static class Util {
+    private static void lookupClassesAndInvokeMethods() {
+      for (String pkg : Arrays.asList("pkg1", "pkg2")) {
+        for (String name : Arrays.asList("A", "B")) {
+          String type = "com.android.tools.r8.keepanno.classpatterns." + pkg + "." + name;
+          try {
+            Class<?> clazz = Class.forName(type);
+            System.out.println(clazz.getDeclaredMethod("foo").invoke(null));
+          } catch (ClassNotFoundException ignored) {
+          } catch (IllegalAccessException ignored) {
+          } catch (InvocationTargetException ignored) {
+          } catch (NoSuchMethodException ignored) {
+          }
+        }
+      }
+    }
+  }
+
+  static class TestAll {
+
+    @UsesReflection({
+      @KeepTarget(
+          kind = KeepItemKind.CLASS_AND_METHODS,
+          // The empty class pattern is equivalent to "any class".
+          classNamePattern = @ClassNamePattern(),
+          methodName = "foo",
+          methodReturnTypeConstant = String.class)
+    })
+    public void foo() throws Exception {
+      Util.lookupClassesAndInvokeMethods();
+    }
+
+    @UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS)
+    public static void main(String[] args) throws Exception {
+      new TestAll().foo();
+    }
+  }
+
+  static class TestPkg {
+
+    @UsesReflection({
+      @KeepTarget(
+          kind = KeepItemKind.CLASS_AND_METHODS,
+          classNamePattern =
+              @ClassNamePattern(packageName = "com.android.tools.r8.keepanno.classpatterns.pkg1"),
+          methodName = "foo",
+          methodReturnTypeConstant = String.class)
+    })
+    public void foo() throws Exception {
+      Util.lookupClassesAndInvokeMethods();
+    }
+
+    @UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS)
+    public static void main(String[] args) throws Exception {
+      new TestPkg().foo();
+    }
+  }
+
+  static class TestName {
+
+    @UsesReflection({
+      @KeepTarget(
+          kind = KeepItemKind.CLASS_AND_METHODS,
+          classNamePattern = @ClassNamePattern(simpleName = "B"),
+          methodName = "foo",
+          methodReturnTypeConstant = String.class)
+    })
+    public void foo() throws Exception {
+      Util.lookupClassesAndInvokeMethods();
+    }
+
+    @UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS)
+    public static void main(String[] args) throws Exception {
+      new TestName().foo();
+    }
+  }
+
+  static class TestSingle {
+
+    @UsesReflection(
+        @KeepTarget(
+            kind = KeepItemKind.CLASS_AND_METHODS,
+            classNamePattern =
+                @ClassNamePattern(
+                    simpleName = "A",
+                    packageName = "com.android.tools.r8.keepanno.classpatterns.pkg2"),
+            methodName = "foo",
+            methodReturnTypePattern =
+                @TypePattern(
+                    classNamePattern =
+                        @ClassNamePattern(packageName = "java.lang", simpleName = "String"))))
+    public void foo() throws Exception {
+      Util.lookupClassesAndInvokeMethods();
+    }
+
+    @UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS)
+    public static void main(String[] args) throws Exception {
+      new TestSingle().foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg1/A.java b/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg1/A.java
new file mode 100644
index 0000000..eab0e5f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg1/A.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2023, 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.classpatterns.pkg1;
+
+public class A {
+
+  public static String foo() {
+    return "pkg1.A";
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg1/B.java b/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg1/B.java
new file mode 100644
index 0000000..edeb6b8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg1/B.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2023, 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.classpatterns.pkg1;
+
+public class B {
+
+  public static String foo() {
+    return "pkg1.B";
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg2/A.java b/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg2/A.java
new file mode 100644
index 0000000..ca0667e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg2/A.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2023, 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.classpatterns.pkg2;
+
+public class A {
+
+  public static String foo() {
+    return "pkg2.A";
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg2/B.java b/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg2/B.java
new file mode 100644
index 0000000..c98e746
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/classpatterns/pkg2/B.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2023, 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.classpatterns.pkg2;
+
+public class B {
+
+  public static String foo() {
+    return "pkg2.B";
+  }
+}
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 bd3d82f..bee5471 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
@@ -9,6 +9,7 @@
 import com.android.tools.r8.cfmethodgeneration.CodeGenerationBase;
 import com.android.tools.r8.keepanno.annotations.CheckOptimizedOut;
 import com.android.tools.r8.keepanno.annotations.CheckRemoved;
+import com.android.tools.r8.keepanno.annotations.ClassNamePattern;
 import com.android.tools.r8.keepanno.annotations.FieldAccessFlags;
 import com.android.tools.r8.keepanno.annotations.KeepBinding;
 import com.android.tools.r8.keepanno.annotations.KeepCondition;
@@ -24,6 +25,7 @@
 import com.android.tools.r8.keepanno.annotations.UsedByNative;
 import com.android.tools.r8.keepanno.annotations.UsedByReflection;
 import com.android.tools.r8.keepanno.annotations.UsesReflection;
+import com.android.tools.r8.keepanno.ast.AnnotationConstants;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.StringUtils.BraceType;
 import com.google.common.base.Strings;
@@ -51,6 +53,11 @@
     Generator.run();
   }
 
+  private static final String DEFAULT_INVALID_TYPE_PATTERN =
+      "@" + simpleName(TypePattern.class) + "(name = \"\")";
+  private static final String DEFAULT_INVALID_CLASS_NAME_PATTERN =
+      "@" + simpleName(ClassNamePattern.class) + "(simpleName = \"\")";
+
   public static String quote(String str) {
     return "\"" + str + "\"";
   }
@@ -69,6 +76,16 @@
       this.name = name;
     }
 
+    public GroupMember setType(String type) {
+      valueType = type;
+      return this;
+    }
+
+    public GroupMember setValue(String value) {
+      valueDefault = value;
+      return this;
+    }
+
     @Override
     public GroupMember self() {
       return this;
@@ -90,50 +107,40 @@
       generator.println("public static final String " + name + " = " + quote(name) + ";");
     }
 
+    public GroupMember requiredValue(Class<?> type) {
+      assert valueDefault == null;
+      return setType(simpleName(type));
+    }
+
+    public GroupMember requiredArrayValue(Class<?> type) {
+      assert valueDefault == null;
+      return setType(simpleName(type) + "[]");
+    }
+
     public GroupMember requiredStringValue() {
-      assert valueDefault == null;
-      return defaultType("String");
+      return requiredValue(String.class);
     }
 
-    public GroupMember requiredValueOfType(String type) {
-      assert valueDefault == null;
-      return defaultType(type);
+    public GroupMember defaultValue(Class<?> type, String value) {
+      setType(simpleName(type));
+      return setValue(value);
     }
 
-    public GroupMember requiredValueOfType(Class<?> type) {
-      assert valueDefault == null;
-      return defaultType(simpleName(type));
-    }
-
-    public GroupMember requiredValueOfArrayType(Class<?> type) {
-      assert valueDefault == null;
-      return defaultType(simpleName(type) + "[]");
-    }
-
-    public GroupMember defaultType(String type) {
-      valueType = type;
-      return this;
-    }
-
-    public GroupMember defaultValue(String value) {
-      valueDefault = value;
-      return this;
+    public GroupMember defaultArrayValue(Class<?> type, String value) {
+      setType(simpleName(type) + "[]");
+      return setValue("{" + value + "}");
     }
 
     public GroupMember defaultEmptyString() {
-      return defaultType("String").defaultValue(quote(""));
+      return defaultValue(String.class, quote(""));
     }
 
     public GroupMember defaultObjectClass() {
-      return defaultType("Class<?>").defaultValue("Object.class");
+      return setType("Class<?>").setValue("Object.class");
     }
 
-    public GroupMember defaultEmptyArray(String valueType) {
-      return defaultType(valueType + "[]").defaultValue("{}");
-    }
-
-    public GroupMember defaultEmptyArray(Class<?> type) {
-      return defaultEmptyArray(simpleName(type));
+    public GroupMember defaultArrayEmpty(Class<?> type) {
+      return defaultArrayValue(type, "");
     }
   }
 
@@ -279,7 +286,7 @@
 
     private Group createBindingsGroup() {
       return new Group("bindings")
-          .addMember(new GroupMember("bindings").defaultEmptyArray(KeepBinding.class));
+          .addMember(new GroupMember("bindings").defaultArrayEmpty(KeepBinding.class));
     }
 
     private Group createPreconditionsGroup() {
@@ -291,7 +298,7 @@
                   .setDocReturn(
                       "The list of preconditions. "
                           + "Defaults to no conditions, thus trivially/unconditionally satisfied.")
-                  .defaultEmptyArray(KeepCondition.class));
+                  .defaultArrayEmpty(KeepCondition.class));
     }
 
     private Group createConsequencesGroup() {
@@ -300,7 +307,7 @@
               new GroupMember("consequences")
                   .setDocTitle("Consequences that must be kept if the annotation is in effect.")
                   .setDocReturn("The list of target consequences.")
-                  .requiredValueOfArrayType(KeepTarget.class));
+                  .requiredArrayValue(KeepTarget.class));
     }
 
     private Group createConsequencesAsValueGroup() {
@@ -309,7 +316,7 @@
               new GroupMember("value")
                   .setDocTitle("Consequences that must be kept if the annotation is in effect.")
                   .setDocReturn("The list of target consequences.")
-                  .requiredValueOfArrayType(KeepTarget.class));
+                  .requiredArrayValue(KeepTarget.class));
     }
 
     private Group createAdditionalPreconditionsGroup() {
@@ -320,7 +327,7 @@
                   .setDocReturn(
                       "The list of additional preconditions. "
                           + "Defaults to no additional preconditions.")
-                  .defaultEmptyArray("KeepCondition"));
+                  .defaultArrayEmpty(KeepCondition.class));
     }
 
     private Group createAdditionalTargetsGroup(String docTitle) {
@@ -331,7 +338,7 @@
                   .setDocReturn(
                       "List of additional target consequences. "
                           + "Defaults to no additional target consequences.")
-                  .defaultEmptyArray("KeepTarget"));
+                  .defaultArrayEmpty(KeepTarget.class));
     }
 
     private Group typePatternGroup() {
@@ -345,7 +352,11 @@
               new GroupMember("constant")
                   .setDocTitle("Exact type from a class constant.")
                   .addParagraph("For example, {@code String.class}.")
-                  .defaultObjectClass());
+                  .defaultObjectClass())
+          .addMember(
+              new GroupMember("classNamePattern")
+                  .setDocTitle("Classes matching the class-name pattern.")
+                  .defaultValue(ClassNamePattern.class, DEFAULT_INVALID_CLASS_NAME_PATTERN));
       // TODO(b/248408342): Add more injections on type pattern variants.
       // /** Exact type name as a string to match any array with that type as member. */
       // String arrayOf() default "";
@@ -366,14 +377,37 @@
       // boolean anyReference() default false;
     }
 
+    private Group classNamePatternSimpleNameGroup() {
+      return new Group("class-simple-name")
+          .addMember(
+              new GroupMember("simpleName")
+                  .setDocTitle("Exact simple name of the class or interface.")
+                  .addParagraph(
+                      "For example, the simple name of {@code com.example.MyClass} is {@code"
+                          + " MyClass}.")
+                  .addParagraph("The default matches any simple name.")
+                  .defaultEmptyString());
+    }
+
+    private Group classNamePatternPackageGroup() {
+      return new Group("class-package-name")
+          .addMember(
+              new GroupMember("packageName")
+                  .setDocTitle("Exact package name of the class or interface.")
+                  .addParagraph(
+                      "For example, the package of {@code com.example.MyClass} is {@code"
+                          + " com.example}.")
+                  .addParagraph("The default matches any package.")
+                  .defaultEmptyString());
+    }
+
     private Group getKindGroup() {
       return new Group(KIND_GROUP).addMember(getKindMember());
     }
 
     private static GroupMember getKindMember() {
       return new GroupMember("kind")
-          .defaultType("KeepItemKind")
-          .defaultValue("KeepItemKind.DEFAULT")
+          .defaultValue(KeepItemKind.class, "KeepItemKind.DEFAULT")
           .setDocTitle("Specify the kind of this item pattern.")
           .setDocReturn("The kind for this pattern.")
           .addParagraph("Possible values are:")
@@ -406,7 +440,7 @@
                       "The specified option constraints do not need to be preserved for the"
                           + " target.")
                   .setDocReturn("Option constraints allowed to be modified for the target.")
-                  .defaultEmptyArray("KeepOption"))
+                  .defaultArrayEmpty(KeepOption.class))
           .addMember(
               new GroupMember("disallow")
                   .setDeprecated("Use " + docLink(constraints()) + " instead.")
@@ -415,7 +449,7 @@
                   .addParagraph(
                       "The specified option constraints *must* be preserved for the target.")
                   .setDocReturn("Option constraints not allowed to be modified for the target.")
-                  .defaultEmptyArray("KeepOption"))
+                  .defaultArrayEmpty(KeepOption.class))
           .addDocFooterParagraph(
               "If nothing is specified for "
                   + CONSTRAINTS_GROUP
@@ -447,7 +481,7 @@
                       KeepConstraint.FIELD_GET,
                       KeepConstraint.FIELD_SET))
           .setDocReturn("Usage constraints for the target.")
-          .defaultEmptyArray(KeepConstraint.class);
+          .defaultArrayEmpty(KeepConstraint.class);
     }
 
     private GroupMember bindingName() {
@@ -487,10 +521,19 @@
           .defaultObjectClass();
     }
 
+    private GroupMember classNamePattern() {
+      return new GroupMember("classNamePattern")
+          .setDocTitle(
+              "Define the " + CLASS_NAME_GROUP + " pattern by reference to a class-name pattern.")
+          .setDocReturn("The class-name pattern that defines the class.")
+          .defaultValue(ClassNamePattern.class, DEFAULT_INVALID_CLASS_NAME_PATTERN);
+    }
+
     private Group createClassNamePatternGroup() {
       return new Group(CLASS_NAME_GROUP)
           .addMember(className())
           .addMember(classConstant())
+          .addMember(classNamePattern())
           .addDocFooterParagraph("If none are specified the default is to match any class name.");
     }
 
@@ -604,7 +647,7 @@
                       "Mutually exclusive with all field and method properties",
                       "as use restricts the match to both types of members.")
                   .setDocReturn("The member access-flag constraints that must be met.")
-                  .defaultEmptyArray("MemberAccessFlags"));
+                  .defaultArrayEmpty(MemberAccessFlags.class));
     }
 
     private String getMutuallyExclusiveForMethodProperties() {
@@ -635,7 +678,7 @@
                   .addParagraph(getMutuallyExclusiveForMethodProperties())
                   .addParagraph(getMethodDefaultDoc("any method-access flags"))
                   .setDocReturn("The method access-flag constraints that must be met.")
-                  .defaultEmptyArray("MethodAccessFlags"));
+                  .defaultArrayEmpty(MethodAccessFlags.class));
     }
 
     private Group createMethodNameGroup() {
@@ -672,8 +715,7 @@
                   .addParagraph(getMutuallyExclusiveForMethodProperties())
                   .addParagraph(getMethodDefaultDoc("any return type"))
                   .setDocReturn("The pattern of the method return type.")
-                  .defaultType("TypePattern")
-                  .defaultValue("@TypePattern(name = \"\")"));
+                  .defaultValue(TypePattern.class, DEFAULT_INVALID_TYPE_PATTERN));
     }
 
     private Group createMethodParametersGroup() {
@@ -685,8 +727,7 @@
                   .addParagraph(getMutuallyExclusiveForMethodProperties())
                   .addParagraph(getMethodDefaultDoc("any parameters"))
                   .setDocReturn("The list of qualified type names of the method parameters.")
-                  .defaultType("String[]")
-                  .defaultValue("{\"\"}"))
+                  .defaultArrayValue(String.class, quote("")))
           .addMember(
               new GroupMember("methodParameterTypePatterns")
                   .setDocTitle(
@@ -694,8 +735,7 @@
                   .addParagraph(getMutuallyExclusiveForMethodProperties())
                   .addParagraph(getMethodDefaultDoc("any parameters"))
                   .setDocReturn("The list of type patterns for the method parameters.")
-                  .defaultType("TypePattern[]")
-                  .defaultValue("{@TypePattern(name = \"\")}"));
+                  .defaultArrayValue(TypePattern.class, DEFAULT_INVALID_TYPE_PATTERN));
     }
 
     private Group createFieldAccessGroup() {
@@ -706,7 +746,7 @@
                   .addParagraph(getMutuallyExclusiveForFieldProperties())
                   .addParagraph(getFieldDefaultDoc("any field-access flags"))
                   .setDocReturn("The field access-flag constraints that must be met.")
-                  .defaultEmptyArray("FieldAccessFlags"));
+                  .defaultArrayEmpty(FieldAccessFlags.class));
     }
 
     private Group createFieldNameGroup() {
@@ -742,8 +782,7 @@
                   .addParagraph(getMutuallyExclusiveForFieldProperties())
                   .addParagraph(getFieldDefaultDoc("any type"))
                   .setDocReturn("The type pattern for the field type.")
-                  .defaultType("TypePattern")
-                  .defaultValue("@TypePattern(name = \"\")"));
+                  .defaultValue(TypePattern.class, DEFAULT_INVALID_TYPE_PATTERN));
     }
 
     private void generateClassAndMemberPropertiesWithClassAndMemberBinding() {
@@ -814,13 +853,37 @@
           .printDoc(this::println);
       println("@Target(ElementType.ANNOTATION_TYPE)");
       println("@Retention(RetentionPolicy.CLASS)");
-      println("public @interface TypePattern {");
+      println("public @interface " + simpleName(TypePattern.class) + " {");
       println();
       withIndent(() -> typePatternGroup().generate(this));
       println();
       println("}");
     }
 
+    private void generateClassNamePattern() {
+      printCopyRight(2023);
+      printPackage("annotations");
+      printImports(ANNOTATION_IMPORTS);
+      DocPrinter.printer()
+          .setDocTitle("A pattern structure for matching names of classes and interfaces.")
+          .addParagraph(
+              "If no properties are set, the default pattern matches any name of a class or"
+                  + " interface.")
+          .printDoc(this::println);
+      println("@Target(ElementType.ANNOTATION_TYPE)");
+      println("@Retention(RetentionPolicy.CLASS)");
+      println("public @interface " + simpleName(ClassNamePattern.class) + " {");
+      println();
+      withIndent(
+          () -> {
+            classNamePatternSimpleNameGroup().generate(this);
+            println();
+            classNamePatternPackageGroup().generate(this);
+          });
+      println();
+      println("}");
+    }
+
     private void generateKeepBinding() {
       printCopyRight(2022);
       printPackage("annotations");
@@ -1151,6 +1214,7 @@
             generateFieldAccessConstants();
 
             generateTypePatternConstants();
+            generateClassNamePatternConstants();
           });
       println("}");
     }
@@ -1431,6 +1495,18 @@
       println();
     }
 
+    private void generateClassNamePatternConstants() {
+      println("public static final class ClassNamePattern {");
+      withIndent(
+          () -> {
+            generateAnnotationConstants(ClassNamePattern.class);
+            classNamePatternSimpleNameGroup().generateConstants(this);
+            classNamePatternPackageGroup().generateConstants(this);
+          });
+      println("}");
+      println();
+    }
+
     private static void writeFile(Path file, Consumer<Generator> fn) throws IOException {
       ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
       PrintStream printStream = new PrintStream(byteStream);
@@ -1443,26 +1519,31 @@
       Files.write(Paths.get(ToolHelper.getProjectRoot()).resolve(file), formatted.getBytes());
     }
 
+    public static Path source(Path pkg, Class<?> clazz) {
+      return pkg.resolve(simpleName(clazz) + ".java");
+    }
+
     public static void run() throws IOException {
       writeFile(Paths.get("doc/keepanno-guide.md"), KeepAnnoMarkdownGenerator::generateMarkdownDoc);
 
       Path keepAnnoRoot = Paths.get("src/keepanno/java/com/android/tools/r8/keepanno");
 
       Path astPkg = keepAnnoRoot.resolve("ast");
-      writeFile(astPkg.resolve("AnnotationConstants.java"), Generator::generateConstants);
+      writeFile(source(astPkg, AnnotationConstants.class), Generator::generateConstants);
 
       Path annoPkg = Paths.get("src/keepanno/java/com/android/tools/r8/keepanno/annotations");
-      writeFile(annoPkg.resolve("TypePattern.java"), Generator::generateTypePattern);
-      writeFile(annoPkg.resolve("KeepBinding.java"), Generator::generateKeepBinding);
-      writeFile(annoPkg.resolve("KeepTarget.java"), Generator::generateKeepTarget);
-      writeFile(annoPkg.resolve("KeepCondition.java"), Generator::generateKeepCondition);
-      writeFile(annoPkg.resolve("KeepForApi.java"), Generator::generateKeepForApi);
-      writeFile(annoPkg.resolve("UsesReflection.java"), Generator::generateUsesReflection);
+      writeFile(source(annoPkg, TypePattern.class), Generator::generateTypePattern);
+      writeFile(source(annoPkg, ClassNamePattern.class), Generator::generateClassNamePattern);
+      writeFile(source(annoPkg, KeepBinding.class), Generator::generateKeepBinding);
+      writeFile(source(annoPkg, KeepTarget.class), Generator::generateKeepTarget);
+      writeFile(source(annoPkg, KeepCondition.class), Generator::generateKeepCondition);
+      writeFile(source(annoPkg, KeepForApi.class), Generator::generateKeepForApi);
+      writeFile(source(annoPkg, UsesReflection.class), Generator::generateUsesReflection);
       writeFile(
-          annoPkg.resolve("UsedByReflection.java"),
+          source(annoPkg, UsedByReflection.class),
           g -> g.generateUsedByX("UsedByReflection", "accessed reflectively"));
       writeFile(
-          annoPkg.resolve("UsedByNative.java"),
+          source(annoPkg, UsedByNative.class),
           g -> g.generateUsedByX("UsedByNative", "accessed from native code via JNI"));
     }
   }