[KeepAnno] Parser for arrays of values

This CL also removes the CRTP idiom as it does not work well with
composing by delegation.

Bug: b/248408342
Change-Id: I76cd79961bee14c0f90b1b20bfba551e6c2c1231
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ArrayPropertyParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ArrayPropertyParser.java
new file mode 100644
index 0000000..3c6cd08
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ArrayPropertyParser.java
@@ -0,0 +1,76 @@
+// 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.ParsingContext;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import org.objectweb.asm.AnnotationVisitor;
+
+public class ArrayPropertyParser<T, P> extends PropertyParserBase<List<T>, P> {
+
+  private final Function<ParsingContext, PropertyParser<T, P>> elementParser;
+  private List<T> values;
+
+  public ArrayPropertyParser(
+      ParsingContext parsingContext, Function<ParsingContext, PropertyParser<T, P>> elementParser) {
+    super(parsingContext);
+    this.elementParser = elementParser;
+  }
+
+  @Override
+  AnnotationVisitor tryPropertyArray(P property, String name, Consumer<List<T>> setValue) {
+    // The property name and type is forwarded to the element parser.
+    values = new ArrayList<>();
+    ParsingContext parsingContext = getParsingContext();
+    return new AnnotationVisitorBase(parsingContext) {
+
+      private PropertyParser<T, P> getParser() {
+        PropertyParser<T, P> parser = elementParser.apply(parsingContext);
+        getMapping().forEach(parser::setProperty);
+        return parser;
+      }
+
+      @Override
+      public void visitEnd() {
+        setValue.accept(values);
+      }
+
+      @Override
+      public void visit(String unusedName, Object value) {
+        if (!getParser().tryParse(name, value, values::add)) {
+          super.visit(name, value);
+        }
+      }
+
+      @Override
+      public AnnotationVisitor visitAnnotation(String unusedName, String descriptor) {
+        AnnotationVisitor visitor = getParser().tryParseAnnotation(name, descriptor, values::add);
+        if (visitor != null) {
+          return visitor;
+        }
+        return super.visitAnnotation(name, descriptor);
+      }
+
+      @Override
+      public void visitEnum(String unusedName, String descriptor, String value) {
+        if (!getParser().tryParseEnum(name, descriptor, value, values::add)) {
+          super.visitEnum(name, descriptor, value);
+        }
+      }
+
+      @Override
+      public AnnotationVisitor visitArray(String unusedName) {
+        AnnotationVisitor visitor = getParser().tryParseArray(name, values::add);
+        if (visitor != null) {
+          return visitor;
+        }
+        return super.visitArray(name);
+      }
+    };
+  }
+}
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
index d6c41ac..6712bfe 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassNameParser.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassNameParser.java
@@ -18,7 +18,7 @@
 import org.objectweb.asm.AnnotationVisitor;
 
 public class ClassNameParser
-    extends PropertyParserBase<KeepQualifiedClassNamePattern, ClassNameProperty, ClassNameParser> {
+    extends PropertyParserBase<KeepQualifiedClassNamePattern, ClassNameProperty> {
 
   public ClassNameParser(ParsingContext parsingContext) {
     super(parsingContext);
@@ -29,11 +29,6 @@
   }
 
   @Override
-  public ClassNameParser self() {
-    return this;
-  }
-
-  @Override
   AnnotationVisitor tryPropertyAnnotation(
       ClassNameProperty property,
       String name,
@@ -44,12 +39,10 @@
         {
           AnnotationParsingContext parsingContext =
               new AnnotationParsingContext(getParsingContext(), descriptor);
-          PackageNameParser packageParser =
-              new PackageNameParser(parsingContext)
-                  .setProperty(PackageNameProperty.NAME, ClassNamePattern.packageName);
-          ClassSimpleNameParser nameParser =
-              new ClassSimpleNameParser(parsingContext)
-                  .setProperty(ClassSimpleNameProperty.NAME, ClassNamePattern.simpleName);
+          PackageNameParser packageParser = new PackageNameParser(parsingContext);
+          ClassSimpleNameParser nameParser = new ClassSimpleNameParser(parsingContext);
+          packageParser.setProperty(ClassNamePattern.packageName, PackageNameProperty.NAME);
+          nameParser.setProperty(ClassNamePattern.simpleName, ClassSimpleNameProperty.NAME);
           return new ParserVisitor(
               parsingContext,
               descriptor,
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
index d349898..ed46972 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassSimpleNameParser.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ClassSimpleNameParser.java
@@ -10,8 +10,7 @@
 import java.util.function.Consumer;
 
 public class ClassSimpleNameParser
-    extends PropertyParserBase<
-        KeepUnqualfiedClassNamePattern, ClassSimpleNameProperty, ClassSimpleNameParser> {
+    extends PropertyParserBase<KeepUnqualfiedClassNamePattern, ClassSimpleNameProperty> {
 
   public ClassSimpleNameParser(ParsingContext parsingContext) {
     super(parsingContext);
@@ -22,11 +21,6 @@
   }
 
   @Override
-  public ClassSimpleNameParser self() {
-    return this;
-  }
-
-  @Override
   public boolean tryProperty(
       ClassSimpleNameProperty property,
       String name,
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/ConvertingPropertyParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ConvertingPropertyParser.java
new file mode 100644
index 0000000..3be9c92
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ConvertingPropertyParser.java
@@ -0,0 +1,72 @@
+// 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 java.util.function.Consumer;
+import java.util.function.Function;
+import org.objectweb.asm.AnnotationVisitor;
+
+/**
+ * Abstract base parser to help converting a parser of one type to another via delegation.
+ *
+ * @param <T1> Type of input parser.
+ * @param <T2> Type of output parser (e.g., the parser subclassing this base).
+ * @param <P> Properties (same for both parsers).
+ */
+public abstract class ConvertingPropertyParser<T1, T2, P> implements PropertyParser<T2, P> {
+
+  private final PropertyParser<T1, P> parser;
+  private final Function<T1, T2> converter;
+
+  public ConvertingPropertyParser(PropertyParser<T1, P> parser, Function<T1, T2> converter) {
+    this.parser = parser;
+    this.converter = converter;
+  }
+
+  private Consumer<T1> wrap(Consumer<T2> fn) {
+    return v1 -> fn.accept(converter.apply(v1));
+  }
+
+  @Override
+  public String kind() {
+    return parser.kind();
+  }
+
+  @Override
+  public void setProperty(String name, P property) {
+    parser.setProperty(name, property);
+  }
+
+  @Override
+  public boolean isDeclared() {
+    return parser.isDeclared();
+  }
+
+  @Override
+  public T2 getValue() {
+    return converter.apply(parser.getValue());
+  }
+
+  @Override
+  public T2 tryParse(String name, Object value) {
+    return converter.apply(parser.tryParse(name, value));
+  }
+
+  @Override
+  public boolean tryParseEnum(String name, String descriptor, String value, Consumer<T2> setValue) {
+    return parser.tryParseEnum(name, descriptor, value, wrap(setValue));
+  }
+
+  @Override
+  public AnnotationVisitor tryParseArray(String name, Consumer<T2> setValue) {
+    return parser.tryParseArray(name, wrap(setValue));
+  }
+
+  @Override
+  public AnnotationVisitor tryParseAnnotation(
+      String name, String descriptor, Consumer<T2> setValue) {
+    return parser.tryParseAnnotation(name, descriptor, wrap(setValue));
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/FieldTypeParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/FieldTypeParser.java
new file mode 100644
index 0000000..176b1cd
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/FieldTypeParser.java
@@ -0,0 +1,18 @@
+// 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.TypeParser.TypeProperty;
+import com.android.tools.r8.keepanno.ast.KeepFieldTypePattern;
+import com.android.tools.r8.keepanno.ast.KeepTypePattern;
+import com.android.tools.r8.keepanno.ast.ParsingContext;
+
+public class FieldTypeParser
+    extends ConvertingPropertyParser<KeepTypePattern, KeepFieldTypePattern, TypeProperty> {
+
+  public FieldTypeParser(ParsingContext parsingContext) {
+    super(new TypeParser(parsingContext), KeepFieldTypePattern::fromType);
+  }
+}
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 00729cb..b49d399 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
@@ -3,6 +3,7 @@
 // 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.TypeParser.TypeProperty;
 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;
@@ -18,7 +19,6 @@
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.MethodAccess;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Option;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Target;
-import com.android.tools.r8.keepanno.ast.AnnotationConstants.TypePattern;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.UsedByReflection;
 import com.android.tools.r8.keepanno.ast.KeepAnnotationParserException;
 import com.android.tools.r8.keepanno.ast.KeepBindingReference;
@@ -62,7 +62,6 @@
 import com.android.tools.r8.keepanno.ast.ParsingContext.ClassParsingContext;
 import com.android.tools.r8.keepanno.ast.ParsingContext.FieldParsingContext;
 import com.android.tools.r8.keepanno.ast.ParsingContext.MethodParsingContext;
-import com.android.tools.r8.keepanno.utils.Unimplemented;
 import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -1533,21 +1532,19 @@
   private static class MethodReturnTypeDeclaration
       extends SingleDeclaration<KeepMethodReturnTypePattern> {
 
-    private final TypeParser typeParser;
+    private final MethodReturnTypeParser typeParser;
 
     private MethodReturnTypeDeclaration(ParsingContext parsingContext) {
       super(parsingContext);
-      typeParser =
-          new TypeParser(parsingContext)
-              .setKind("return type")
-              .enableTypePattern(Item.methodReturnTypePattern)
-              .enableTypeName(Item.methodReturnType)
-              .enableTypeConstant(Item.methodReturnTypeConstant);
+      typeParser = new MethodReturnTypeParser(parsingContext);
+      typeParser.setProperty(Item.methodReturnType, TypeProperty.TYPE_NAME);
+      typeParser.setProperty(Item.methodReturnTypeConstant, TypeProperty.TYPE_CONSTANT);
+      typeParser.setProperty(Item.methodReturnTypePattern, TypeProperty.TYPE_PATTERN);
     }
 
     @Override
     String kind() {
-      return "return type";
+      return typeParser.kind();
     }
 
     @Override
@@ -1555,54 +1552,33 @@
       return KeepMethodReturnTypePattern.any();
     }
 
-    KeepMethodReturnTypePattern fromType(KeepTypePattern typePattern) {
-      if (typePattern == null) {
-        return null;
-      }
-      // Special-case method return types to allow void.
-      String descriptor = typePattern.getDescriptor();
-      if (descriptor.equals("V") || descriptor.equals("Lvoid;")) {
-        return KeepMethodReturnTypePattern.voidType();
-      }
-      return KeepMethodReturnTypePattern.fromType(typePattern);
-    }
-
     @Override
     public KeepMethodReturnTypePattern parse(String name, Object value) {
-      return fromType(typeParser.tryParse(name, value));
+      return typeParser.tryParse(name, value);
     }
 
     @Override
     public AnnotationVisitor parseAnnotation(
         String name, String descriptor, Consumer<KeepMethodReturnTypePattern> setValue) {
-      return typeParser.tryParseAnnotation(name, descriptor, t -> setValue.accept(fromType(t)));
+      return typeParser.tryParseAnnotation(name, descriptor, setValue);
     }
   }
 
   private static class MethodParametersDeclaration
       extends SingleDeclaration<KeepMethodParametersPattern> {
 
-    private final ParsingContext parsingContext;
-    private KeepMethodParametersPattern pattern = null;
+    private final MethodParametersParser parser;
 
     public MethodParametersDeclaration(ParsingContext parsingContext) {
       super(parsingContext);
-      this.parsingContext = parsingContext;
-    }
-
-    private void setPattern(
-        KeepMethodParametersPattern pattern, Consumer<KeepMethodParametersPattern> setValue) {
-      assert setValue != null;
-      if (this.pattern != null) {
-        throw parsingContext.error("Cannot declare multiple patterns for the parameter list");
-      }
-      setValue.accept(pattern);
-      this.pattern = pattern;
+      parser = new MethodParametersParser(parsingContext);
+      parser.setProperty(Item.methodParameters, TypeProperty.TYPE_NAME);
+      parser.setProperty(Item.methodParameterTypePatterns, TypeProperty.TYPE_PATTERN);
     }
 
     @Override
     String kind() {
-      return "parameters";
+      return parser.kind();
     }
 
     @Override
@@ -1617,29 +1593,7 @@
 
     @Override
     AnnotationVisitor parseArray(String name, Consumer<KeepMethodParametersPattern> setValue) {
-      if (name.equals(Item.methodParameters)) {
-        return new StringArrayVisitor(
-            getParsingContext(),
-            params -> {
-              KeepMethodParametersPattern.Builder builder = KeepMethodParametersPattern.builder();
-              for (String param : params) {
-                builder.addParameterTypePattern(KeepEdgeReaderUtils.typePatternFromString(param));
-              }
-              setPattern(builder.build(), setValue);
-            });
-      }
-      if (name.equals(Item.methodParameterTypePatterns)) {
-        return new TypePatternsArrayVisitor(
-            getParsingContext(),
-            params -> {
-              KeepMethodParametersPattern.Builder builder = KeepMethodParametersPattern.builder();
-              for (KeepTypePattern param : params) {
-                builder.addParameterTypePattern(param);
-              }
-              setPattern(builder.build(), setValue);
-            });
-      }
-      return super.parseArray(name, setValue);
+      return parser.tryParseArray(name, setValue);
     }
   }
 
@@ -1717,16 +1671,14 @@
 
   private static class FieldTypeDeclaration extends SingleDeclaration<KeepFieldTypePattern> {
 
-    private final TypeParser typeParser;
+    private final FieldTypeParser typeParser;
 
     private FieldTypeDeclaration(ParsingContext parsingContext) {
       super(parsingContext);
-      this.typeParser =
-          new TypeParser(parsingContext)
-              .setKind("field type")
-              .enableTypePattern(Item.fieldTypePattern)
-              .enableTypeName(Item.fieldType)
-              .enableTypeConstant(Item.fieldTypeConstant);
+      typeParser = new FieldTypeParser(parsingContext);
+      typeParser.setProperty(Item.fieldTypePattern, TypeProperty.TYPE_PATTERN);
+      typeParser.setProperty(Item.fieldType, TypeProperty.TYPE_NAME);
+      typeParser.setProperty(Item.fieldTypeConstant, TypeProperty.TYPE_CONSTANT);
     }
 
     @Override
@@ -1741,18 +1693,13 @@
 
     @Override
     public KeepFieldTypePattern parse(String name, Object value) {
-      KeepTypePattern typePattern = typeParser.tryParse(name, value);
-      if (typePattern != null) {
-        return KeepFieldTypePattern.fromType(typePattern);
-      }
-      return null;
+      return typeParser.tryParse(name, value);
     }
 
     @Override
     public AnnotationVisitor parseAnnotation(
         String name, String descriptor, Consumer<KeepFieldTypePattern> setValue) {
-      return typeParser.tryParseAnnotation(
-          name, descriptor, t -> setValue.accept(KeepFieldTypePattern.fromType(t)));
+      return typeParser.tryParseAnnotation(name, descriptor, setValue);
     }
   }
 
@@ -2314,89 +2261,6 @@
     }
   }
 
-  private static class TypePatternVisitor extends AnnotationVisitorBase {
-    private final ParsingContext parsingContext;
-    private final Consumer<KeepTypePattern> consumer;
-    private KeepTypePattern result = null;
-
-    private TypePatternVisitor(ParsingContext parsingContext, Consumer<KeepTypePattern> consumer) {
-      super(parsingContext);
-      this.parsingContext = parsingContext;
-      this.consumer = consumer;
-    }
-
-    private void setResult(KeepTypePattern result) {
-      if (this.result != null) {
-        throw parsingContext.error("Invalid type annotation defining multiple properties.");
-      }
-      this.result = result;
-    }
-
-    @Override
-    public void visit(String name, Object value) {
-      if (TypePattern.name.equals(name) && value instanceof String) {
-        setResult(KeepEdgeReaderUtils.typePatternFromString((String) value));
-        return;
-      }
-      if (TypePattern.constant.equals(name) && value instanceof Type) {
-        Type type = (Type) value;
-        setResult(KeepTypePattern.fromDescriptor(type.getDescriptor()));
-        return;
-      }
-      super.visit(name, value);
-    }
-
-    @Override
-    public AnnotationVisitor visitAnnotation(String name, String descriptor) {
-      if (TypePattern.classNamePattern.equals(name)
-          && descriptor.equals(ClassNamePattern.DESCRIPTOR)) {
-        return new ClassNamePatternVisitor(
-            new AnnotationParsingContext(parsingContext, descriptor),
-            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 implemented yet");
-              }
-            });
-      }
-      return super.visitAnnotation(name, descriptor);
-    }
-
-    @Override
-    public void visitEnd() {
-      consumer.accept(result != null ? result : KeepTypePattern.any());
-    }
-  }
-
-  private static class TypePatternsArrayVisitor extends AnnotationVisitorBase {
-    private final ParsingContext parsingContext;
-    private final Consumer<List<KeepTypePattern>> fn;
-    private final List<KeepTypePattern> patterns = new ArrayList<>();
-
-    public TypePatternsArrayVisitor(
-        ParsingContext parsingContext, Consumer<List<KeepTypePattern>> fn) {
-      super(parsingContext);
-      this.parsingContext = parsingContext;
-      this.fn = fn;
-    }
-
-    @Override
-    public AnnotationVisitor visitAnnotation(String unusedName, String descriptor) {
-      if (TypePattern.DESCRIPTOR.equals(descriptor)) {
-        return new TypePatternVisitor(parsingContext, patterns::add);
-      }
-      return null;
-    }
-
-    @Override
-    public void visitEnd() {
-      super.visitEnd();
-      fn.accept(patterns);
-    }
-  }
-
   private static class OptionsDeclaration extends SingleDeclaration<KeepOptions> {
 
     public OptionsDeclaration(ParsingContext parsingContext) {
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/MethodParametersParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/MethodParametersParser.java
new file mode 100644
index 0000000..f1cba56
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/MethodParametersParser.java
@@ -0,0 +1,30 @@
+// 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.TypeParser.TypeProperty;
+import com.android.tools.r8.keepanno.ast.KeepMethodParametersPattern;
+import com.android.tools.r8.keepanno.ast.KeepTypePattern;
+import com.android.tools.r8.keepanno.ast.ParsingContext;
+import java.util.List;
+
+public class MethodParametersParser
+    extends ConvertingPropertyParser<
+        List<KeepTypePattern>, KeepMethodParametersPattern, TypeProperty> {
+
+  public MethodParametersParser(ParsingContext parsingContext) {
+    super(
+        new ArrayPropertyParser<>(parsingContext, TypeParser::new),
+        MethodParametersParser::convert);
+  }
+
+  private static KeepMethodParametersPattern convert(List<KeepTypePattern> params) {
+    KeepMethodParametersPattern.Builder builder = KeepMethodParametersPattern.builder();
+    for (KeepTypePattern param : params) {
+      builder.addParameterTypePattern(param);
+    }
+    return builder.build();
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/MethodReturnTypeParser.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/MethodReturnTypeParser.java
new file mode 100644
index 0000000..8a87a70
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/MethodReturnTypeParser.java
@@ -0,0 +1,33 @@
+// 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.TypeParser.TypeProperty;
+import com.android.tools.r8.keepanno.ast.KeepMethodReturnTypePattern;
+import com.android.tools.r8.keepanno.ast.KeepTypePattern;
+import com.android.tools.r8.keepanno.ast.ParsingContext;
+
+public class MethodReturnTypeParser
+    extends ConvertingPropertyParser<KeepTypePattern, KeepMethodReturnTypePattern, TypeProperty> {
+
+  public MethodReturnTypeParser(ParsingContext parsingContext) {
+    super(new TypeParser(parsingContext), MethodReturnTypeParser::fromType);
+  }
+
+  // TODO(b/248408342): It may be problematic dealing with the "void" value at the point of return
+  //  as it is not actually a type in the normal sense. Consider a set up where the "void" cases are
+  //  special cased before dispatch to the underlying type parser.
+  private static KeepMethodReturnTypePattern fromType(KeepTypePattern typePattern) {
+    if (typePattern == null) {
+      return null;
+    }
+    // Special-case method return types to allow void.
+    String descriptor = typePattern.getDescriptor();
+    if (descriptor.equals("V") || descriptor.equals("Lvoid;")) {
+      return KeepMethodReturnTypePattern.voidType();
+    }
+    return KeepMethodReturnTypePattern.fromType(typePattern);
+  }
+}
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
index dadb8de..9c9295d 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/PackageNameParser.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/PackageNameParser.java
@@ -9,8 +9,7 @@
 import com.android.tools.r8.keepanno.ast.ParsingContext;
 import java.util.function.Consumer;
 
-public class PackageNameParser
-    extends PropertyParserBase<KeepPackagePattern, PackageNameProperty, PackageNameParser> {
+public class PackageNameParser extends PropertyParserBase<KeepPackagePattern, PackageNameProperty> {
 
   public PackageNameParser(ParsingContext parsingContext) {
     super(parsingContext);
@@ -21,11 +20,6 @@
   }
 
   @Override
-  public PackageNameParser self() {
-    return this;
-  }
-
-  @Override
   public boolean tryProperty(
       PackageNameProperty property,
       String name,
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 77d6087..5f1f69b 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
@@ -12,13 +12,13 @@
 /** Convert parser(s) into an annotation visitor. */
 public class ParserVisitor extends AnnotationVisitorBase {
 
-  private final List<PropertyParser<?, ?, ?>> parsers;
+  private final List<PropertyParser<?, ?>> parsers;
   private final Runnable onVisitEnd;
 
   public ParserVisitor(
       AnnotationParsingContext parsingContext,
       String annotationDescriptor,
-      List<PropertyParser<?, ?, ?>> parsers,
+      List<PropertyParser<?, ?>> parsers,
       Runnable onVisitEnd) {
     super(parsingContext);
     this.parsers = parsers;
@@ -29,7 +29,7 @@
   public ParserVisitor(
       AnnotationParsingContext parsingContext,
       String annotationDescriptor,
-      PropertyParser<?, ?, ?> declaration,
+      PropertyParser<?, ?> declaration,
       Runnable onVisitEnd) {
     this(parsingContext, annotationDescriptor, Collections.singletonList(declaration), onVisitEnd);
   }
@@ -38,7 +38,7 @@
 
   @Override
   public void visit(String name, Object value) {
-    for (PropertyParser<?, ?, ?> parser : parsers) {
+    for (PropertyParser<?, ?> parser : parsers) {
       if (parser.tryParse(name, value, this::ignore)) {
         return;
       }
@@ -48,7 +48,7 @@
 
   @Override
   public AnnotationVisitor visitArray(String name) {
-    for (PropertyParser<?, ?, ?> parser : parsers) {
+    for (PropertyParser<?, ?> parser : parsers) {
       AnnotationVisitor visitor = parser.tryParseArray(name, this::ignore);
       if (visitor != null) {
         return visitor;
@@ -59,7 +59,7 @@
 
   @Override
   public void visitEnum(String name, String descriptor, String value) {
-    for (PropertyParser<?, ?, ?> parser : parsers) {
+    for (PropertyParser<?, ?> parser : parsers) {
       if (parser.tryParseEnum(name, descriptor, value, this::ignore)) {
         return;
       }
@@ -69,7 +69,7 @@
 
   @Override
   public AnnotationVisitor visitAnnotation(String name, String descriptor) {
-    for (PropertyParser<?, ?, ?> parser : parsers) {
+    for (PropertyParser<?, ?> parser : parsers) {
       AnnotationVisitor visitor = parser.tryParseAnnotation(name, descriptor, this::ignore);
       if (visitor != null) {
         return visitor;
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 67b181b..4dc7b38 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
@@ -7,13 +7,11 @@
 import java.util.function.Consumer;
 import org.objectweb.asm.AnnotationVisitor;
 
-public interface PropertyParser<T, P, S> {
-
-  S self();
+public interface PropertyParser<T, P> {
 
   String kind();
 
-  S setProperty(P property, String name);
+  void setProperty(String name, P property);
 
   boolean isDeclared();
 
@@ -23,7 +21,16 @@
 
   T getValue();
 
-  boolean tryParse(String name, Object value, Consumer<T> setValue);
+  T tryParse(String name, Object value);
+
+  default boolean tryParse(String name, Object value, Consumer<T> setValue) {
+    T result = tryParse(name, value);
+    if (result != null) {
+      setValue.accept(result);
+      return true;
+    }
+    return false;
+  }
 
   boolean tryParseEnum(String name, String descriptor, String value, 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
index 108f086..0982c9f 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParserBase.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParserBase.java
@@ -12,7 +12,7 @@
 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> {
+public abstract class PropertyParserBase<T, P> implements PropertyParser<T, P> {
 
   private final ParsingContext parsingContext;
 
@@ -29,6 +29,10 @@
     return parsingContext;
   }
 
+  Map<String, P> getMapping() {
+    return mapping;
+  }
+
   boolean tryProperty(P property, String name, Object value, Consumer<T> setValue) {
     return false;
   }
@@ -87,38 +91,30 @@
     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) {
+  public void setKind(String kind) {
     this.kind = kind;
-    return self();
   }
 
   /** Add property parsing for the given property-name. */
-  public S setProperty(P property, String name) {
+  public void setProperty(String name, P property) {
     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) {
+  public final T tryParse(String name, Object value) {
     P prop = mapping.get(name);
     if (prop != null) {
-      return tryProperty(prop, name, value, wrap(name, setValue));
+      tryProperty(prop, name, value, wrap(name, unused -> {}));
+      return resultValue;
     }
-    return false;
+    return null;
   }
 
   @Override
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 6b11f70..e3922c6 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
@@ -15,40 +15,19 @@
 import org.objectweb.asm.AnnotationVisitor;
 import org.objectweb.asm.Type;
 
-public class TypeParser extends PropertyParserBase<KeepTypePattern, TypeProperty, TypeParser> {
+public class TypeParser extends PropertyParserBase<KeepTypePattern, TypeProperty> {
 
   public TypeParser(ParsingContext parsingContext) {
     super(parsingContext);
   }
 
   public enum TypeProperty {
-    SELF_PATTERN,
+    TYPE_PATTERN,
     TYPE_NAME,
     TYPE_CONSTANT,
     CLASS_NAME_PATTERN
   }
 
-  public TypeParser enableTypePattern(String propertyName) {
-    return setProperty(TypeProperty.SELF_PATTERN, propertyName);
-  }
-
-  public TypeParser enableTypeName(String propertyName) {
-    return setProperty(TypeProperty.TYPE_NAME, propertyName);
-  }
-
-  public TypeParser enableTypeConstant(String propertyName) {
-    return setProperty(TypeProperty.TYPE_CONSTANT, propertyName);
-  }
-
-  public TypeParser enableTypeClassNamePattern(String propertyName) {
-    return setProperty(TypeProperty.CLASS_NAME_PATTERN, propertyName);
-  }
-
-  @Override
-  public TypeParser self() {
-    return this;
-  }
-
   @Override
   public boolean tryProperty(
       TypeProperty property, String name, Object value, Consumer<KeepTypePattern> setValue) {
@@ -68,16 +47,15 @@
   public AnnotationVisitor tryPropertyAnnotation(
       TypeProperty property, String name, String descriptor, Consumer<KeepTypePattern> setValue) {
     switch (property) {
-      case SELF_PATTERN:
+      case TYPE_PATTERN:
         {
           AnnotationParsingContext parsingContext =
               new AnnotationParsingContext(getParsingContext(), descriptor);
-          TypeParser typeParser =
-              new TypeParser(parsingContext)
-                  .setKind(kind())
-                  .enableTypeName(TypePattern.name)
-                  .enableTypeConstant(TypePattern.constant)
-                  .enableTypeClassNamePattern(TypePattern.classNamePattern);
+          TypeParser typeParser = new TypeParser(parsingContext);
+          typeParser.setKind(kind());
+          typeParser.setProperty(TypePattern.name, TypeProperty.TYPE_NAME);
+          typeParser.setProperty(TypePattern.constant, TypeProperty.TYPE_CONSTANT);
+          typeParser.setProperty(TypePattern.classNamePattern, TypeProperty.CLASS_NAME_PATTERN);
           return new ParserVisitor(
               parsingContext,
               descriptor,
@@ -86,21 +64,21 @@
         }
       case CLASS_NAME_PATTERN:
         {
-          return new ClassNameParser(getParsingContext())
-              .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");
-                    }
-                  });
+          ClassNameParser parser = new ClassNameParser(getParsingContext());
+          parser.setKind(kind());
+          return parser.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;