[KeepAnno] Extend parser to composite definitions.

This makes the "single value" parser the base implementation and
pulls out the interface. The visitor is extended to support
composing parsers.

Bug: b/248408342
Change-Id: I5e068f4d376108e57781336d3e6a01d48466426d
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassNameParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassNameParser.java
new file mode 100644
index 0000000..c1e5655
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassNameParser.java
@@ -0,0 +1,61 @@
+// 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.asm;
+
+import com.android.tools.r8.keepanno.asm.ClassNameParser.ClassNameProperty;
+import com.android.tools.r8.keepanno.asm.ClassSimpleNameParser.ClassSimpleNameProperty;
+import com.android.tools.r8.keepanno.asm.PackageNameParser.PackageNameProperty;
+import com.android.tools.r8.keepanno.ast.AnnotationConstants.ClassNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepPackagePattern;
+import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepUnqualfiedClassNamePattern;
+import com.google.common.collect.ImmutableList;
+import java.util.function.Consumer;
+import org.objectweb.asm.AnnotationVisitor;
+
+public class ClassNameParser
+    extends PropertyParserBase<KeepQualifiedClassNamePattern, ClassNameProperty, ClassNameParser> {
+
+  public enum ClassNameProperty {
+    PATTERN
+  }
+
+  @Override
+  public ClassNameParser self() {
+    return this;
+  }
+
+  @Override
+  AnnotationVisitor tryPropertyAnnotation(
+      ClassNameProperty property,
+      String name,
+      String descriptor,
+      Consumer<KeepQualifiedClassNamePattern> setValue) {
+    switch (property) {
+      case PATTERN:
+        {
+          PackageNameParser packageParser =
+              new PackageNameParser()
+                  .setProperty(PackageNameProperty.NAME, ClassNamePattern.packageName);
+          ClassSimpleNameParser nameParser =
+              new ClassSimpleNameParser()
+                  .setProperty(ClassSimpleNameProperty.NAME, ClassNamePattern.simpleName);
+          return new ParserVisitor(
+              descriptor,
+              ImmutableList.of(packageParser, nameParser),
+              () ->
+                  setValue.accept(
+                      KeepQualifiedClassNamePattern.builder()
+                          .setPackagePattern(
+                              packageParser.getValueOrDefault(KeepPackagePattern.any()))
+                          .setNamePattern(
+                              nameParser.getValueOrDefault(KeepUnqualfiedClassNamePattern.any()))
+                          .build()));
+        }
+      default:
+        return null;
+    }
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassSimpleNameParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassSimpleNameParser.java
new file mode 100644
index 0000000..17296bc
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassSimpleNameParser.java
@@ -0,0 +1,38 @@
+// 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.asm;
+
+import com.android.tools.r8.keepanno.asm.ClassSimpleNameParser.ClassSimpleNameProperty;
+import com.android.tools.r8.keepanno.ast.KeepUnqualfiedClassNamePattern;
+import java.util.function.Consumer;
+
+public class ClassSimpleNameParser
+    extends PropertyParserBase<
+        KeepUnqualfiedClassNamePattern, ClassSimpleNameProperty, ClassSimpleNameParser> {
+
+  public enum ClassSimpleNameProperty {
+    NAME
+  }
+
+  @Override
+  public ClassSimpleNameParser self() {
+    return this;
+  }
+
+  @Override
+  public boolean tryProperty(
+      ClassSimpleNameProperty property,
+      String name,
+      Object value,
+      Consumer<KeepUnqualfiedClassNamePattern> setValue) {
+    switch (property) {
+      case NAME:
+        setValue.accept(KeepUnqualfiedClassNamePattern.exact((String) value));
+        return true;
+      default:
+        return false;
+    }
+  }
+}
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 2ba3cbf..4513464 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
@@ -2275,7 +2275,7 @@
                 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");
+                throw new Unimplemented("Non-exact class patterns are not implemented yet");
               }
             });
       }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/PackageNameParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/PackageNameParser.java
new file mode 100644
index 0000000..adef04f
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/PackageNameParser.java
@@ -0,0 +1,37 @@
+// 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.asm;
+
+import com.android.tools.r8.keepanno.asm.PackageNameParser.PackageNameProperty;
+import com.android.tools.r8.keepanno.ast.KeepPackagePattern;
+import java.util.function.Consumer;
+
+public class PackageNameParser
+    extends PropertyParserBase<KeepPackagePattern, PackageNameProperty, PackageNameParser> {
+
+  public enum PackageNameProperty {
+    NAME
+  }
+
+  @Override
+  public PackageNameParser self() {
+    return this;
+  }
+
+  @Override
+  public boolean tryProperty(
+      PackageNameProperty property,
+      String name,
+      Object value,
+      Consumer<KeepPackagePattern> setValue) {
+    switch (property) {
+      case NAME:
+        setValue.accept(KeepPackagePattern.exact((String) value));
+        return true;
+      default:
+        return false;
+    }
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ParserVisitor.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ParserVisitor.java
index e95494b..5c8ea6f 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ParserVisitor.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ParserVisitor.java
@@ -4,25 +4,31 @@
 
 package com.android.tools.r8.keepanno.asm;
 
-import java.util.function.Consumer;
+import java.util.Collections;
+import java.util.List;
 import org.objectweb.asm.AnnotationVisitor;
 
-/** Convert a parser into an annotation visitor. */
-public class ParserVisitor<T, P, S> extends AnnotationVisitorBase {
+/** Convert parser(s) into an annotation visitor. */
+public class ParserVisitor extends AnnotationVisitorBase {
 
   private final String annotationDescriptor;
-  private final SingleValuePropertyParser<T, P, S> declaration;
-  private final Consumer<T> callback;
+  private final List<PropertyParser<?, ?, ?>> parsers;
+  private final Runnable onVisitEnd;
 
   public ParserVisitor(
-      String annotationDescriptor,
-      SingleValuePropertyParser<T, P, S> declaration,
-      Consumer<T> callback) {
+      String annotationDescriptor, List<PropertyParser<?, ?, ?>> parsers, Runnable onVisitEnd) {
     this.annotationDescriptor = annotationDescriptor;
-    this.declaration = declaration;
-    this.callback = callback;
+    this.parsers = parsers;
+    this.onVisitEnd = onVisitEnd;
   }
 
+  public ParserVisitor(
+      String annotationDescriptor, PropertyParser<?, ?, ?> declaration, Runnable onVisitEnd) {
+    this(annotationDescriptor, Collections.singletonList(declaration), onVisitEnd);
+  }
+
+  private <T> void ignore(T unused) {}
+
   @Override
   public String getAnnotationName() {
     int start = annotationDescriptor.lastIndexOf('/') + 1;
@@ -32,39 +38,49 @@
 
   @Override
   public void visit(String name, Object value) {
-    if (declaration.tryParse(name, value, unused -> {})) {
-      return;
+    for (PropertyParser<?, ?, ?> parser : parsers) {
+      if (parser.tryParse(name, value, this::ignore)) {
+        return;
+      }
     }
     super.visit(name, value);
   }
 
   @Override
   public AnnotationVisitor visitArray(String name) {
-    AnnotationVisitor visitor = declaration.tryParseArray(name, unused -> {});
-    if (visitor != null) {
-      return visitor;
+    for (PropertyParser<?, ?, ?> parser : parsers) {
+      AnnotationVisitor visitor = parser.tryParseArray(name, this::ignore);
+      if (visitor != null) {
+        return visitor;
+      }
     }
     return super.visitArray(name);
   }
 
   @Override
   public void visitEnum(String name, String descriptor, String value) {
+    for (PropertyParser<?, ?, ?> parser : parsers) {
+      if (parser.tryParseEnum(name, descriptor, value, this::ignore)) {
+        return;
+      }
+    }
     super.visitEnum(name, descriptor, value);
   }
 
   @Override
   public AnnotationVisitor visitAnnotation(String name, String descriptor) {
-    AnnotationVisitor visitor = declaration.tryParseAnnotation(name, descriptor, unused -> {});
-    if (visitor != null) {
-      return visitor;
+    for (PropertyParser<?, ?, ?> parser : parsers) {
+      AnnotationVisitor visitor = parser.tryParseAnnotation(name, descriptor, this::ignore);
+      if (visitor != null) {
+        return visitor;
+      }
     }
     return super.visitAnnotation(name, descriptor);
   }
 
   @Override
   public void visitEnd() {
-    if (declaration.isDeclared()) {
-      callback.accept(declaration.getSingleValue());
-    }
+    onVisitEnd.run();
+    super.visitEnd();
   }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParser.java
index 70160a3..67b181b 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParser.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParser.java
@@ -4,68 +4,30 @@
 
 package com.android.tools.r8.keepanno.asm;
 
-import java.util.HashMap;
-import java.util.Map;
 import java.util.function.Consumer;
 import org.objectweb.asm.AnnotationVisitor;
 
-public abstract class PropertyParser<T, P, S> {
+public interface PropertyParser<T, P, S> {
 
-  private String kind;
-  private final Map<String, P> mapping = new HashMap<>();
+  S self();
 
-  abstract S self();
+  String kind();
 
-  abstract boolean tryProperty(P property, String name, Object value, Consumer<T> setValue);
+  S setProperty(P property, String name);
 
-  abstract AnnotationVisitor tryPropertyArray(P property, String name, Consumer<T> setValue);
+  boolean isDeclared();
 
-  abstract AnnotationVisitor tryPropertyAnnotation(
-      P property, String name, String descriptor, Consumer<T> setValue);
-
-  String kind() {
-    return kind != null ? kind : "";
+  default boolean isDefault() {
+    return !isDeclared();
   }
 
-  public S setKind(String kind) {
-    this.kind = kind;
-    return self();
-  }
+  T getValue();
 
-  /** Add property parsing for the given property-name. */
-  public S setProperty(P property, String name) {
-    P old = mapping.put(name, property);
-    if (old != null) {
-      throw new IllegalArgumentException("Unexpected attempt to redefine property " + name);
-    }
-    return self();
-  }
+  boolean tryParse(String name, Object value, Consumer<T> setValue);
 
-  /** Parse a property. Returns true if the property-name triggered parsing. */
-  public final boolean tryParse(String name, Object value, Consumer<T> setValue) {
-    P prop = mapping.get(name);
-    if (prop != null) {
-      return tryProperty(prop, name, value, setValue);
-    }
-    return false;
-  }
+  boolean tryParseEnum(String name, String descriptor, String value, Consumer<T> setValue);
 
-  /** Parse a property. Returns non-null if the property-name triggered parsing. */
-  public final AnnotationVisitor tryParseArray(String name, Consumer<T> setValue) {
-    P prop = mapping.get(name);
-    if (prop != null) {
-      return tryPropertyArray(prop, name, setValue);
-    }
-    return null;
-  }
+  AnnotationVisitor tryParseArray(String name, Consumer<T> setValue);
 
-  /** Parse a property. Returns non-null if the property-name triggered parsing. */
-  public final AnnotationVisitor tryParseAnnotation(
-      String name, String descriptor, Consumer<T> setValue) {
-    P prop = mapping.get(name);
-    if (prop != null) {
-      return tryPropertyAnnotation(prop, name, descriptor, setValue);
-    }
-    return null;
-  }
+  AnnotationVisitor tryParseAnnotation(String name, String descriptor, Consumer<T> setValue);
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParserBase.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParserBase.java
new file mode 100644
index 0000000..027b6c2
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParserBase.java
@@ -0,0 +1,141 @@
+// 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.asm;
+
+import com.android.tools.r8.keepanno.ast.KeepEdgeException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+import org.objectweb.asm.AnnotationVisitor;
+
+/** Special case of a property parser allowing only a single value callback. */
+public abstract class PropertyParserBase<T, P, S> implements PropertyParser<T, P, S> {
+
+  private String kind;
+  private final Map<String, P> mapping = new HashMap<>();
+  private String resultPropertyName = null;
+  private T resultValue = null;
+
+  boolean tryProperty(P property, String name, Object value, Consumer<T> setValue) {
+    return false;
+  }
+
+  public boolean tryPropertyEnum(
+      P property, String name, String descriptor, String value, Consumer<T> setValue) {
+    return false;
+  }
+
+  AnnotationVisitor tryPropertyArray(P property, String name, Consumer<T> setValue) {
+    return null;
+  }
+
+  AnnotationVisitor tryPropertyAnnotation(
+      P property, String name, String descriptor, Consumer<T> setValue) {
+    return null;
+  }
+
+  private Consumer<T> wrap(String propertyName, Consumer<T> setValue) {
+    return value -> {
+      assert value != null;
+      if (resultPropertyName != null) {
+        assert resultValue != null;
+        error(propertyName);
+      } else {
+        resultPropertyName = propertyName;
+        resultValue = value;
+        setValue.accept(value);
+      }
+    };
+  }
+
+  private void error(String name) {
+    throw new KeepEdgeException(
+        "Multiple properties defining "
+            + kind()
+            + ": '"
+            + resultPropertyName
+            + "' and '"
+            + name
+            + "'");
+  }
+
+  public final boolean isDeclared() {
+    assert (resultPropertyName != null) == (resultValue != null);
+    return resultPropertyName != null;
+  }
+
+  public T getValue() {
+    assert (resultPropertyName != null) == (resultValue != null);
+    return resultValue;
+  }
+
+  public T getValueOrDefault(T defaultValue) {
+    assert (resultPropertyName != null) == (resultValue != null);
+    return isDeclared() ? resultValue : defaultValue;
+  }
+
+  /** Helper for parsing directly. Returns non-null if the property-name triggered parsing. */
+  public final T tryParse(String name, Object value) {
+    boolean triggered = tryParse(name, value, unused -> {});
+    assert triggered == (resultValue != null);
+    return resultValue;
+  }
+
+  public String kind() {
+    return kind != null ? kind : "";
+  }
+
+  public S setKind(String kind) {
+    this.kind = kind;
+    return self();
+  }
+
+  /** Add property parsing for the given property-name. */
+  public S setProperty(P property, String name) {
+    P old = mapping.put(name, property);
+    if (old != null) {
+      throw new IllegalArgumentException("Unexpected attempt to redefine property " + name);
+    }
+    return self();
+  }
+
+  @Override
+  public final boolean tryParse(String name, Object value, Consumer<T> setValue) {
+    P prop = mapping.get(name);
+    if (prop != null) {
+      return tryProperty(prop, name, value, wrap(name, setValue));
+    }
+    return false;
+  }
+
+  @Override
+  public final boolean tryParseEnum(
+      String name, String descriptor, String value, Consumer<T> setValue) {
+    P prop = mapping.get(name);
+    if (prop != null) {
+      return tryPropertyEnum(prop, name, descriptor, value, wrap(name, setValue));
+    }
+    return false;
+  }
+
+  @Override
+  public final AnnotationVisitor tryParseArray(String name, Consumer<T> setValue) {
+    P prop = mapping.get(name);
+    if (prop != null) {
+      return tryPropertyArray(prop, name, wrap(name, setValue));
+    }
+    return null;
+  }
+
+  @Override
+  public final AnnotationVisitor tryParseAnnotation(
+      String name, String descriptor, Consumer<T> setValue) {
+    P prop = mapping.get(name);
+    if (prop != null) {
+      return tryPropertyAnnotation(prop, name, descriptor, wrap(name, setValue));
+    }
+    return null;
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/SingleValuePropertyParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/SingleValuePropertyParser.java
deleted file mode 100644
index dcb8088..0000000
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/SingleValuePropertyParser.java
+++ /dev/null
@@ -1,81 +0,0 @@
-// 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.asm;
-
-import com.android.tools.r8.keepanno.ast.KeepEdgeException;
-import java.util.function.Consumer;
-import org.objectweb.asm.AnnotationVisitor;
-
-/** Special case of a property parser allowing only a single value callback. */
-public abstract class SingleValuePropertyParser<T, P, S> extends PropertyParser<T, P, S> {
-
-  private String resultPropertyName = null;
-  private T resultValue = null;
-
-  abstract boolean trySingleProperty(P property, String name, Object value, Consumer<T> setValue);
-
-  abstract AnnotationVisitor trySinglePropertyArray(P property, String name, Consumer<T> setValue);
-
-  abstract AnnotationVisitor trySinglePropertyAnnotation(
-      P property, String name, String descriptor, Consumer<T> setValue);
-
-  private Consumer<T> wrap(String propertyName, Consumer<T> setValue) {
-    return value -> {
-      assert value != null;
-      if (resultPropertyName != null) {
-        assert resultValue != null;
-        error(propertyName);
-      } else {
-        resultPropertyName = propertyName;
-        resultValue = value;
-        setValue.accept(value);
-      }
-    };
-  }
-
-  private void error(String name) {
-    throw new KeepEdgeException(
-        "Multiple properties defining "
-            + kind()
-            + ": '"
-            + resultPropertyName
-            + "' and '"
-            + name
-            + "'");
-  }
-
-  public final boolean isDeclared() {
-    assert (resultPropertyName != null) == (resultValue != null);
-    return resultPropertyName != null;
-  }
-
-  public T getSingleValue() {
-    assert (resultPropertyName != null) == (resultValue != null);
-    return resultValue;
-  }
-
-  /** Helper for parsing directly. Returns non-null if the property-name triggered parsing. */
-  public final T tryParse(String name, Object value) {
-    boolean triggered = tryParse(name, value, unused -> {});
-    assert triggered == (resultValue != null);
-    return resultValue;
-  }
-
-  @Override
-  final boolean tryProperty(P property, String name, Object value, Consumer<T> setValue) {
-    return trySingleProperty(property, name, value, wrap(name, setValue));
-  }
-
-  @Override
-  final AnnotationVisitor tryPropertyArray(P property, String name, Consumer<T> setValue) {
-    return trySinglePropertyArray(property, name, wrap(name, setValue));
-  }
-
-  @Override
-  final AnnotationVisitor tryPropertyAnnotation(
-      P property, String name, String descriptor, Consumer<T> setValue) {
-    return trySinglePropertyAnnotation(property, name, descriptor, wrap(name, setValue));
-  }
-}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/TypeParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/TypeParser.java
index dd9b475..f9c6739 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/TypeParser.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/TypeParser.java
@@ -4,7 +4,8 @@
 
 package com.android.tools.r8.keepanno.asm;
 
-import com.android.tools.r8.keepanno.asm.TypeParser.Properties;
+import com.android.tools.r8.keepanno.asm.ClassNameParser.ClassNameProperty;
+import com.android.tools.r8.keepanno.asm.TypeParser.TypeProperty;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.TypePattern;
 import com.android.tools.r8.keepanno.ast.KeepTypePattern;
 import com.android.tools.r8.keepanno.utils.Unimplemented;
@@ -12,9 +13,9 @@
 import org.objectweb.asm.AnnotationVisitor;
 import org.objectweb.asm.Type;
 
-public class TypeParser extends SingleValuePropertyParser<KeepTypePattern, Properties, TypeParser> {
+public class TypeParser extends PropertyParserBase<KeepTypePattern, TypeProperty, TypeParser> {
 
-  public enum Properties {
+  public enum TypeProperty {
     SELF_PATTERN,
     TYPE_NAME,
     TYPE_CONSTANT,
@@ -22,29 +23,29 @@
   }
 
   public TypeParser enableTypePattern(String propertyName) {
-    return setProperty(Properties.SELF_PATTERN, propertyName);
+    return setProperty(TypeProperty.SELF_PATTERN, propertyName);
   }
 
   public TypeParser enableTypeName(String propertyName) {
-    return setProperty(Properties.TYPE_NAME, propertyName);
+    return setProperty(TypeProperty.TYPE_NAME, propertyName);
   }
 
   public TypeParser enableTypeConstant(String propertyName) {
-    return setProperty(Properties.TYPE_CONSTANT, propertyName);
+    return setProperty(TypeProperty.TYPE_CONSTANT, propertyName);
   }
 
   public TypeParser enableTypeClassNamePattern(String propertyName) {
-    return setProperty(Properties.CLASS_NAME_PATTERN, propertyName);
+    return setProperty(TypeProperty.CLASS_NAME_PATTERN, propertyName);
   }
 
   @Override
-  TypeParser self() {
+  public TypeParser self() {
     return this;
   }
 
   @Override
-  public boolean trySingleProperty(
-      Properties property, String name, Object value, Consumer<KeepTypePattern> setValue) {
+  public boolean tryProperty(
+      TypeProperty property, String name, Object value, Consumer<KeepTypePattern> setValue) {
     switch (property) {
       case TYPE_NAME:
         setValue.accept(KeepEdgeReaderUtils.typePatternFromString((String) value));
@@ -58,26 +59,36 @@
   }
 
   @Override
-  public AnnotationVisitor trySinglePropertyArray(
-      Properties property, String name, Consumer<KeepTypePattern> setValue) {
-    return null;
-  }
-
-  @Override
-  public AnnotationVisitor trySinglePropertyAnnotation(
-      Properties property, String name, String descriptor, Consumer<KeepTypePattern> setValue) {
+  public AnnotationVisitor tryPropertyAnnotation(
+      TypeProperty property, String name, String descriptor, Consumer<KeepTypePattern> setValue) {
     switch (property) {
       case SELF_PATTERN:
-        return new ParserVisitor<>(
-            descriptor,
+        TypeParser typeParser =
             new TypeParser()
                 .setKind(kind())
                 .enableTypeName(TypePattern.name)
                 .enableTypeConstant(TypePattern.constant)
-                .enableTypeClassNamePattern(TypePattern.classNamePattern),
-            setValue);
+                .enableTypeClassNamePattern(TypePattern.classNamePattern);
+        return new ParserVisitor(
+            descriptor,
+            typeParser,
+            () -> setValue.accept(typeParser.getValueOrDefault(KeepTypePattern.any())));
       case CLASS_NAME_PATTERN:
-        throw new Unimplemented("Non-exact class patterns are not unimplemented yet");
+        return new ClassNameParser()
+            .setKind(kind())
+            .tryPropertyAnnotation(
+                ClassNameProperty.PATTERN,
+                name,
+                descriptor,
+                classNamePattern -> {
+                  if (classNamePattern.isExact()) {
+                    setValue.accept(
+                        KeepTypePattern.fromDescriptor(classNamePattern.getExactDescriptor()));
+                  } else {
+                    // TODO(b/248408342): Extend the AST type patterns.
+                    throw new Unimplemented("Non-exact class patterns are not implemented yet");
+                  }
+                });
       default:
         return null;
     }