[KeepAnno] Introduce a reusable parser structure

The added parser differs from "declarations" by having a mapping of
property names to the logical parser cases. This allows sharing the
parsing of the annotation `TypePattern.name` with the external
properties such as `fieldType` and `methodReturnType`.

Follow-up CLs will extend the parser for composition and then replace
all the "declarations" and annotation visitors.

Bug: b/248408342
Change-Id: Ia95e0fa662f0b73c677c64c454c240d10644dcba
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/AnnotationVisitorBase.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/AnnotationVisitorBase.java
new file mode 100644
index 0000000..e15d4e7
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/AnnotationVisitorBase.java
@@ -0,0 +1,59 @@
+// 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 org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.Type;
+
+public abstract class AnnotationVisitorBase extends AnnotationVisitor {
+
+  AnnotationVisitorBase() {
+    super(KeepEdgeReader.ASM_VERSION);
+  }
+
+  public abstract String getAnnotationName();
+
+  private String errorMessagePrefix() {
+    return "@" + getAnnotationName() + ": ";
+  }
+
+  private String getTypeName(String descriptor) {
+    return Type.getType(descriptor).getClassName();
+  }
+
+  @Override
+  public void visit(String name, Object value) {
+    throw new KeepEdgeException(
+        "Unexpected value in " + errorMessagePrefix() + name + " = " + value);
+  }
+
+  @Override
+  public AnnotationVisitor visitAnnotation(String name, String descriptor) {
+    throw new KeepEdgeException(
+        "Unexpected annotation in "
+            + errorMessagePrefix()
+            + name
+            + " for annotation: "
+            + getTypeName(descriptor));
+  }
+
+  @Override
+  public void visitEnum(String name, String descriptor, String value) {
+    throw new KeepEdgeException(
+        "Unexpected enum in "
+            + errorMessagePrefix()
+            + name
+            + " for enum: "
+            + getTypeName(descriptor)
+            + " with value: "
+            + value);
+  }
+
+  @Override
+  public AnnotationVisitor visitArray(String name) {
+    throw new KeepEdgeException("Unexpected array in " + errorMessagePrefix() + name);
+  }
+}
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 cee27a8..2ba3cbf 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
@@ -416,40 +416,6 @@
     void accept(T result);
   }
 
-  private abstract static class AnnotationVisitorBase extends AnnotationVisitor {
-
-    AnnotationVisitorBase() {
-      super(ASM_VERSION);
-    }
-
-    public abstract String getAnnotationName();
-
-    private String errorMessagePrefix() {
-      return " @" + getAnnotationName() + ": ";
-    }
-
-    @Override
-    public void visit(String name, Object value) {
-      throw new KeepEdgeException(
-          "Unexpected value in" + errorMessagePrefix() + name + " = " + value);
-    }
-
-    @Override
-    public AnnotationVisitor visitAnnotation(String name, String descriptor) {
-      throw new KeepEdgeException("Unexpected annotation in" + errorMessagePrefix() + name);
-    }
-
-    @Override
-    public void visitEnum(String name, String descriptor, String value) {
-      throw new KeepEdgeException("Unexpected enum in" + errorMessagePrefix() + name);
-    }
-
-    @Override
-    public AnnotationVisitor visitArray(String name) {
-      throw new KeepEdgeException("Unexpected array in" + errorMessagePrefix() + name);
-    }
-  }
-
   private static class UserBindingsHelper {
     private final KeepBindings.Builder builder = KeepBindings.builder();
     private final Map<String, KeepBindingSymbol> userNames = new HashMap<>();
@@ -1491,9 +1457,16 @@
       extends SingleDeclaration<KeepMethodReturnTypePattern> {
 
     private final Supplier<String> annotationName;
+    private final TypeParser typeParser;
 
     private MethodReturnTypeDeclaration(Supplier<String> annotationName) {
       this.annotationName = annotationName;
+      typeParser =
+          new TypeParser()
+              .setKind("return type")
+              .enableTypePattern(Item.methodReturnTypePattern)
+              .enableTypeName(Item.methodReturnType)
+              .enableTypeConstant(Item.methodReturnTypeConstant);
     }
 
     @Override
@@ -1506,26 +1479,27 @@
       return KeepMethodReturnTypePattern.any();
     }
 
-    @Override
-    KeepMethodReturnTypePattern parse(String name, Object value) {
-      if (name.equals(Item.methodReturnType) && value instanceof String) {
-        return KeepEdgeReaderUtils.methodReturnTypeFromTypeName((String) value);
+    KeepMethodReturnTypePattern fromType(KeepTypePattern typePattern) {
+      if (typePattern == null) {
+        return null;
       }
-      if (name.equals(Item.methodReturnTypeConstant) && value instanceof Type) {
-        Type type = (Type) value;
-        return KeepEdgeReaderUtils.methodReturnTypeFromTypeDescriptor(type.getDescriptor());
+      // Special-case method return types to allow void.
+      String descriptor = typePattern.getDescriptor();
+      if (descriptor.equals("V") || descriptor.equals("Lvoid;")) {
+        return KeepMethodReturnTypePattern.voidType();
       }
-      return null;
+      return KeepMethodReturnTypePattern.fromType(typePattern);
     }
 
     @Override
-    AnnotationVisitor parseAnnotation(
+    public KeepMethodReturnTypePattern parse(String name, Object value) {
+      return fromType(typeParser.tryParse(name, value));
+    }
+
+    @Override
+    public AnnotationVisitor parseAnnotation(
         String name, String descriptor, Consumer<KeepMethodReturnTypePattern> setValue) {
-      if (name.equals(Item.methodReturnTypePattern) && descriptor.equals(TypePattern.DESCRIPTOR)) {
-        return new TypePatternVisitor(
-            annotationName, t -> setValue.accept(KeepMethodReturnTypePattern.fromType(t)));
-      }
-      return super.parseAnnotation(name, descriptor, setValue);
+      return typeParser.tryParseAnnotation(name, descriptor, t -> setValue.accept(fromType(t)));
     }
   }
 
@@ -1666,9 +1640,16 @@
   private static class FieldTypeDeclaration extends SingleDeclaration<KeepFieldTypePattern> {
 
     private final Supplier<String> annotationName;
+    private final TypeParser typeParser;
 
     private FieldTypeDeclaration(Supplier<String> annotationName) {
       this.annotationName = annotationName;
+      this.typeParser =
+          new TypeParser()
+              .setKind("field type")
+              .enableTypePattern(Item.fieldTypePattern)
+              .enableTypeName(Item.fieldType)
+              .enableTypeConstant(Item.fieldTypeConstant);
     }
 
     @Override
@@ -1682,26 +1663,19 @@
     }
 
     @Override
-    KeepFieldTypePattern parse(String name, Object value) {
-      if (name.equals(Item.fieldType) && value instanceof String) {
-        return KeepFieldTypePattern.fromType(
-            KeepEdgeReaderUtils.typePatternFromString((String) value));
-      }
-      if (name.equals(Item.fieldTypeConstant) && value instanceof Type) {
-        String descriptor = ((Type) value).getDescriptor();
-        return KeepFieldTypePattern.fromType(KeepTypePattern.fromDescriptor(descriptor));
+    public KeepFieldTypePattern parse(String name, Object value) {
+      KeepTypePattern typePattern = typeParser.tryParse(name, value);
+      if (typePattern != null) {
+        return KeepFieldTypePattern.fromType(typePattern);
       }
       return null;
     }
 
     @Override
-    AnnotationVisitor parseAnnotation(
+    public AnnotationVisitor parseAnnotation(
         String name, String descriptor, Consumer<KeepFieldTypePattern> setValue) {
-      if (name.equals(Item.fieldTypePattern) && descriptor.equals(TypePattern.DESCRIPTOR)) {
-        return new TypePatternVisitor(
-            annotationName, t -> setValue.accept(KeepFieldTypePattern.fromType(t)));
-      }
-      return super.parseAnnotation(name, descriptor, setValue);
+      return typeParser.tryParseAnnotation(
+          name, descriptor, t -> setValue.accept(KeepFieldTypePattern.fromType(t)));
     }
   }
 
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
new file mode 100644
index 0000000..e95494b
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/ParserVisitor.java
@@ -0,0 +1,70 @@
+// 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 org.objectweb.asm.AnnotationVisitor;
+
+/** Convert a parser into an annotation visitor. */
+public class ParserVisitor<T, P, S> extends AnnotationVisitorBase {
+
+  private final String annotationDescriptor;
+  private final SingleValuePropertyParser<T, P, S> declaration;
+  private final Consumer<T> callback;
+
+  public ParserVisitor(
+      String annotationDescriptor,
+      SingleValuePropertyParser<T, P, S> declaration,
+      Consumer<T> callback) {
+    this.annotationDescriptor = annotationDescriptor;
+    this.declaration = declaration;
+    this.callback = callback;
+  }
+
+  @Override
+  public String getAnnotationName() {
+    int start = annotationDescriptor.lastIndexOf('/') + 1;
+    int end = annotationDescriptor.length() - 1;
+    return annotationDescriptor.substring(start, end);
+  }
+
+  @Override
+  public void visit(String name, Object value) {
+    if (declaration.tryParse(name, value, unused -> {})) {
+      return;
+    }
+    super.visit(name, value);
+  }
+
+  @Override
+  public AnnotationVisitor visitArray(String name) {
+    AnnotationVisitor visitor = declaration.tryParseArray(name, unused -> {});
+    if (visitor != null) {
+      return visitor;
+    }
+    return super.visitArray(name);
+  }
+
+  @Override
+  public void visitEnum(String name, String descriptor, String value) {
+    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;
+    }
+    return super.visitAnnotation(name, descriptor);
+  }
+
+  @Override
+  public void visitEnd() {
+    if (declaration.isDeclared()) {
+      callback.accept(declaration.getSingleValue());
+    }
+  }
+}
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
new file mode 100644
index 0000000..70160a3
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/PropertyParser.java
@@ -0,0 +1,71 @@
+// 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.HashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+import org.objectweb.asm.AnnotationVisitor;
+
+public abstract class PropertyParser<T, P, S> {
+
+  private String kind;
+  private final Map<String, P> mapping = new HashMap<>();
+
+  abstract S self();
+
+  abstract boolean tryProperty(P property, String name, Object value, Consumer<T> setValue);
+
+  abstract AnnotationVisitor tryPropertyArray(P property, String name, Consumer<T> setValue);
+
+  abstract AnnotationVisitor tryPropertyAnnotation(
+      P property, String name, String descriptor, Consumer<T> setValue);
+
+  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();
+  }
+
+  /** 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;
+  }
+
+  /** 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;
+  }
+
+  /** 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;
+  }
+}
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
new file mode 100644
index 0000000..dcb8088
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/SingleValuePropertyParser.java
@@ -0,0 +1,81 @@
+// 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
new file mode 100644
index 0000000..dd9b475
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/TypeParser.java
@@ -0,0 +1,85 @@
+// 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.Properties;
+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;
+import java.util.function.Consumer;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.Type;
+
+public class TypeParser extends SingleValuePropertyParser<KeepTypePattern, Properties, TypeParser> {
+
+  public enum Properties {
+    SELF_PATTERN,
+    TYPE_NAME,
+    TYPE_CONSTANT,
+    CLASS_NAME_PATTERN
+  }
+
+  public TypeParser enableTypePattern(String propertyName) {
+    return setProperty(Properties.SELF_PATTERN, propertyName);
+  }
+
+  public TypeParser enableTypeName(String propertyName) {
+    return setProperty(Properties.TYPE_NAME, propertyName);
+  }
+
+  public TypeParser enableTypeConstant(String propertyName) {
+    return setProperty(Properties.TYPE_CONSTANT, propertyName);
+  }
+
+  public TypeParser enableTypeClassNamePattern(String propertyName) {
+    return setProperty(Properties.CLASS_NAME_PATTERN, propertyName);
+  }
+
+  @Override
+  TypeParser self() {
+    return this;
+  }
+
+  @Override
+  public boolean trySingleProperty(
+      Properties property, String name, Object value, Consumer<KeepTypePattern> setValue) {
+    switch (property) {
+      case TYPE_NAME:
+        setValue.accept(KeepEdgeReaderUtils.typePatternFromString((String) value));
+        return true;
+      case TYPE_CONSTANT:
+        setValue.accept(KeepTypePattern.fromDescriptor(((Type) value).getDescriptor()));
+        return true;
+      default:
+        return false;
+    }
+  }
+
+  @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) {
+    switch (property) {
+      case SELF_PATTERN:
+        return new ParserVisitor<>(
+            descriptor,
+            new TypeParser()
+                .setKind(kind())
+                .enableTypeName(TypePattern.name)
+                .enableTypeConstant(TypePattern.constant)
+                .enableTypeClassNamePattern(TypePattern.classNamePattern),
+            setValue);
+      case CLASS_NAME_PATTERN:
+        throw new Unimplemented("Non-exact class patterns are not unimplemented yet");
+      default:
+        return null;
+    }
+  }
+}