[KeepAnno] Extend types with patterns for primitives, arrays and classes

Bug: b/248408342
Change-Id: Ib6cd70cc883e4d3efdafcb12060a407d0c276e77
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 3034d7a..9137b3e 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
@@ -11,9 +11,11 @@
 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.KeepTypePattern;
 import com.android.tools.r8.keepanno.ast.KeepUnqualfiedClassNamePattern;
 import com.android.tools.r8.keepanno.ast.ParsingContext;
 import com.android.tools.r8.keepanno.ast.ParsingContext.AnnotationParsingContext;
+import com.android.tools.r8.keepanno.ast.ParsingContext.PropertyParsingContext;
 import com.google.common.collect.ImmutableList;
 import java.util.function.Consumer;
 import org.objectweb.asm.AnnotationVisitor;
@@ -44,23 +46,32 @@
                 TypeProperty.TYPE_NAME,
                 name,
                 value,
-                type ->
-                    setValue.accept(
-                        KeepQualifiedClassNamePattern.exactFromDescriptor(type.getDescriptor())));
+                type -> setValue.accept(typeToClassType(type, getParsingContext().property(name))));
       case CONSTANT:
         return new TypeParser(getParsingContext())
             .tryProperty(
                 TypeProperty.TYPE_CONSTANT,
                 name,
                 value,
-                type ->
-                    setValue.accept(
-                        KeepQualifiedClassNamePattern.exactFromDescriptor(type.getDescriptor())));
+                type -> setValue.accept(typeToClassType(type, getParsingContext().property(name))));
       default:
         return false;
     }
   }
 
+  KeepQualifiedClassNamePattern typeToClassType(
+      KeepTypePattern typePattern, PropertyParsingContext parsingContext) {
+    return typePattern.match(
+        KeepQualifiedClassNamePattern::any,
+        primitiveTypePattern -> {
+          throw parsingContext.error("Invalid use of primitive type where class type was expected");
+        },
+        arrayTypePattern -> {
+          throw parsingContext.error("Invalid use of array type where class type was expected");
+        },
+        classNamePattern -> classNamePattern);
+  }
+
   @Override
   AnnotationVisitor tryPropertyAnnotation(
       ClassNameProperty property,
@@ -71,7 +82,7 @@
       case PATTERN:
         {
           AnnotationParsingContext parsingContext =
-              new AnnotationParsingContext(getParsingContext(), descriptor);
+              getParsingContext().property(name).annotation(descriptor);
           PackageNameParser packageParser = new PackageNameParser(parsingContext);
           ClassSimpleNameParser nameParser = new ClassSimpleNameParser(parsingContext);
           packageParser.setProperty(ClassNamePattern.packageName, PackageNameProperty.NAME);
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReaderUtils.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReaderUtils.java
index 6f963c4..21645d5 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReaderUtils.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReaderUtils.java
@@ -3,9 +3,9 @@
 // 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 com.android.tools.r8.keepanno.ast.KeepMethodReturnTypePattern;
 import com.android.tools.r8.keepanno.ast.KeepTypePattern;
+import com.android.tools.r8.keepanno.ast.ParsingContext.PropertyParsingContext;
+import java.util.function.Function;
 
 /**
  * Utilities for mapping the syntax used in annotations to the keep-edge AST.
@@ -57,14 +57,20 @@
     throw new IllegalStateException("Unexpected descriptor: " + descriptor);
   }
 
-  public static KeepTypePattern typePatternFromString(String string) {
+  public static KeepTypePattern typePatternFromString(
+      String string, PropertyParsingContext property) {
     if (string.equals("<any>")) {
       return KeepTypePattern.any();
     }
-    return KeepTypePattern.fromDescriptor(getDescriptorFromJavaType(string));
+    return KeepTypePattern.fromDescriptor(internalDescriptorFromJavaType(string, property::error));
   }
 
   public static String getDescriptorFromJavaType(String type) {
+    return internalDescriptorFromJavaType(type, IllegalStateException::new);
+  }
+
+  private static String internalDescriptorFromJavaType(
+      String type, Function<String, RuntimeException> onError) {
     switch (type) {
       case "boolean":
         return "Z";
@@ -87,11 +93,11 @@
           StringBuilder builder = new StringBuilder(type.length());
           int i = type.length() - 1;
           if (i < 0) {
-            throw new KeepEdgeException("Invalid empty type");
+            throw onError.apply("Invalid empty type");
           }
           while (type.charAt(i) == ']') {
             if (type.charAt(--i) != '[') {
-              throw new KeepEdgeException("Invalid type: '" + type + "'");
+              throw onError.apply("Invalid type: '" + type + "'");
             }
             builder.append('[');
             --i;
@@ -106,19 +112,4 @@
         }
     }
   }
-
-  public static KeepMethodReturnTypePattern methodReturnTypeFromTypeName(String returnType) {
-    if ("void".equals(returnType)) {
-      return KeepMethodReturnTypePattern.voidType();
-    }
-    return KeepMethodReturnTypePattern.fromType(typePatternFromString(returnType));
-  }
-
-  public static KeepMethodReturnTypePattern methodReturnTypeFromTypeDescriptor(
-      String returnTypeDesc) {
-    if ("V".equals(returnTypeDesc)) {
-      return KeepMethodReturnTypePattern.voidType();
-    }
-    return KeepMethodReturnTypePattern.fromType(KeepTypePattern.fromDescriptor(returnTypeDesc));
-  }
 }
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
index 8a87a70..ccf5183 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/MethodReturnTypeParser.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/MethodReturnTypeParser.java
@@ -8,26 +8,74 @@
 import com.android.tools.r8.keepanno.ast.KeepMethodReturnTypePattern;
 import com.android.tools.r8.keepanno.ast.KeepTypePattern;
 import com.android.tools.r8.keepanno.ast.ParsingContext;
+import java.util.function.Consumer;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.Type;
 
+/**
+ * Parser for parsing method return types.
+ *
+ * <p>This parser wraps a type parser and adds support for parsing the string name {@code "void"} or
+ * the class constant {@code void.class} as the void method return type.
+ */
 public class MethodReturnTypeParser
-    extends ConvertingPropertyParser<KeepTypePattern, KeepMethodReturnTypePattern, TypeProperty> {
+    extends PropertyParserBase<KeepMethodReturnTypePattern, TypeProperty> {
+
+  private final TypeParser typeParser;
 
   public MethodReturnTypeParser(ParsingContext parsingContext) {
-    super(new TypeParser(parsingContext), MethodReturnTypeParser::fromType);
+    super(parsingContext);
+    typeParser = new TypeParser(parsingContext);
   }
 
-  // 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;
+  static Consumer<KeepTypePattern> wrap(Consumer<KeepMethodReturnTypePattern> fn) {
+    return t -> fn.accept(KeepMethodReturnTypePattern.fromType(t));
+  }
+
+  @Override
+  public KeepMethodReturnTypePattern getValue() {
+    return super.getValue();
+  }
+
+  @Override
+  boolean tryProperty(
+      TypeProperty property,
+      String name,
+      Object value,
+      Consumer<KeepMethodReturnTypePattern> setValue) {
+    if (property == TypeProperty.TYPE_NAME && "void".equals(value)) {
+      setValue.accept(KeepMethodReturnTypePattern.voidType());
+      return true;
     }
-    // Special-case method return types to allow void.
-    String descriptor = typePattern.getDescriptor();
-    if (descriptor.equals("V") || descriptor.equals("Lvoid;")) {
-      return KeepMethodReturnTypePattern.voidType();
+    if (property == TypeProperty.TYPE_CONSTANT && Type.getType("V").equals(value)) {
+      setValue.accept(KeepMethodReturnTypePattern.voidType());
+      return true;
     }
-    return KeepMethodReturnTypePattern.fromType(typePattern);
+    return typeParser.tryProperty(property, name, value, wrap(setValue));
+  }
+
+  @Override
+  public boolean tryPropertyEnum(
+      TypeProperty property,
+      String name,
+      String descriptor,
+      String value,
+      Consumer<KeepMethodReturnTypePattern> setValue) {
+    return typeParser.tryPropertyEnum(property, name, descriptor, value, wrap(setValue));
+  }
+
+  @Override
+  AnnotationVisitor tryPropertyArray(
+      TypeProperty property, String name, Consumer<KeepMethodReturnTypePattern> setValue) {
+    return typeParser.tryPropertyArray(property, name, wrap(setValue));
+  }
+
+  @Override
+  AnnotationVisitor tryPropertyAnnotation(
+      TypeProperty property,
+      String name,
+      String descriptor,
+      Consumer<KeepMethodReturnTypePattern> setValue) {
+    return typeParser.tryPropertyAnnotation(property, name, descriptor, wrap(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 c1682c9..a40f450 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
@@ -10,7 +10,6 @@
 import com.android.tools.r8.keepanno.ast.KeepTypePattern;
 import com.android.tools.r8.keepanno.ast.ParsingContext;
 import com.android.tools.r8.keepanno.ast.ParsingContext.AnnotationParsingContext;
-import com.android.tools.r8.keepanno.utils.Unimplemented;
 import java.util.function.Consumer;
 import org.objectweb.asm.AnnotationVisitor;
 import org.objectweb.asm.Type;
@@ -33,7 +32,9 @@
       TypeProperty property, String name, Object value, Consumer<KeepTypePattern> setValue) {
     switch (property) {
       case TYPE_NAME:
-        setValue.accept(KeepEdgeReaderUtils.typePatternFromString((String) value));
+        setValue.accept(
+            KeepEdgeReaderUtils.typePatternFromString(
+                (String) value, getParsingContext().property(name)));
         return true;
       case TYPE_CONSTANT:
         setValue.accept(KeepTypePattern.fromDescriptor(((Type) value).getDescriptor()));
@@ -49,15 +50,14 @@
     switch (property) {
       case TYPE_PATTERN:
         {
-          AnnotationParsingContext parsingContext =
-              new AnnotationParsingContext(getParsingContext(), descriptor);
-          TypeParser typeParser =
-              new TypeParser(parsingContext.group(TypePattern.typePatternGroup));
+          AnnotationParsingContext context =
+              getParsingContext().property(name).annotation(descriptor);
+          TypeParser typeParser = new TypeParser(context);
           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,
+              context,
               descriptor,
               typeParser,
               () -> setValue.accept(typeParser.getValueOrDefault(KeepTypePattern.any())));
@@ -69,15 +69,7 @@
               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");
-                }
-              });
+              value -> setValue.accept(KeepTypePattern.fromClass(value)));
         }
       default:
         return null;
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepArrayTypePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepArrayTypePattern.java
new file mode 100644
index 0000000..96bdd8a
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepArrayTypePattern.java
@@ -0,0 +1,56 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.keepanno.ast;
+
+import java.util.Objects;
+
+public class KeepArrayTypePattern {
+
+  private static final KeepArrayTypePattern ANY =
+      new KeepArrayTypePattern(KeepTypePattern.any(), 1);
+
+  public static KeepArrayTypePattern getAny() {
+    return ANY;
+  }
+
+  private final KeepTypePattern baseType;
+  private final int dimensions;
+
+  public KeepArrayTypePattern(KeepTypePattern baseType, int dimensions) {
+    assert baseType != null;
+    assert dimensions > 0;
+    this.baseType = baseType;
+    this.dimensions = dimensions;
+  }
+
+  public boolean isAny() {
+    return ANY.equals(this);
+  }
+
+  public KeepTypePattern getBaseType() {
+    return baseType;
+  }
+
+  public int getDimensions() {
+    return dimensions;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof KeepArrayTypePattern)) {
+      return false;
+    }
+    KeepArrayTypePattern that = (KeepArrayTypePattern) o;
+    return dimensions == that.dimensions && Objects.equals(baseType, that.baseType);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(baseType, dimensions);
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java
index 34dcab5..04485d7 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java
@@ -44,7 +44,15 @@
  *   CLASS_ITEM_PATTERN ::= class QUALIFIED_CLASS_NAME_PATTERN instance-of INSTANCE_OF_PATTERN
  *   MEMBER_ITEM_PATTERN ::= CLASS_ITEM_REFERENCE { MEMBER_PATTERN }
  *
- *   TYPE_PATTERN ::= any | exact type-descriptor
+ *   TYPE_PATTERN
+ *     ::= any
+ *       | PRIMITIVE_TYPE_PATTERN
+ *       | ARRAY_TYPE_PATTERN
+ *       | QUALIFIED_CLASS_NAME_PATTERN
+ *
+ *   PRIMITIVE_TYPE_PATTERN ::= any | boolean | byte | char | short | int | long | float | double
+ *   ARRAY_TYPE_PATTERN ::= any | TYPE_PATTERN dimensions(N > 0)
+ *
  *   PACKAGE_PATTERN ::= any | exact package-name
  *
  *   QUALIFIED_CLASS_NAME_PATTERN
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPrimitiveTypePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPrimitiveTypePattern.java
new file mode 100644
index 0000000..f79093d
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPrimitiveTypePattern.java
@@ -0,0 +1,95 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.keepanno.ast;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import java.util.function.Consumer;
+
+public class KeepPrimitiveTypePattern {
+
+  private static final KeepPrimitiveTypePattern ANY = new KeepPrimitiveTypePattern('*');
+  private static final KeepPrimitiveTypePattern BOOLEAN = new KeepPrimitiveTypePattern('Z');
+  private static final KeepPrimitiveTypePattern BYTE = new KeepPrimitiveTypePattern('B');
+  private static final KeepPrimitiveTypePattern CHAR = new KeepPrimitiveTypePattern('C');
+  private static final KeepPrimitiveTypePattern SHORT = new KeepPrimitiveTypePattern('S');
+  private static final KeepPrimitiveTypePattern INT = new KeepPrimitiveTypePattern('I');
+  private static final KeepPrimitiveTypePattern LONG = new KeepPrimitiveTypePattern('J');
+  private static final KeepPrimitiveTypePattern FLOAT = new KeepPrimitiveTypePattern('F');
+  private static final KeepPrimitiveTypePattern DOUBLE = new KeepPrimitiveTypePattern('D');
+
+  private static final Map<String, KeepPrimitiveTypePattern> PRIMITIVES =
+      populate(BOOLEAN, BYTE, CHAR, SHORT, INT, LONG, FLOAT, DOUBLE);
+
+  private static ImmutableMap<String, KeepPrimitiveTypePattern> populate(
+      KeepPrimitiveTypePattern... types) {
+    ImmutableMap.Builder<String, KeepPrimitiveTypePattern> builder = ImmutableMap.builder();
+    for (KeepPrimitiveTypePattern type : types) {
+      builder.put(type.getDescriptor(), type);
+    }
+    return builder.build();
+  }
+
+  public static KeepPrimitiveTypePattern getAny() {
+    return ANY;
+  }
+
+  public static KeepPrimitiveTypePattern getBoolean() {
+    return BOOLEAN;
+  }
+
+  public static KeepPrimitiveTypePattern getByte() {
+    return BYTE;
+  }
+
+  public static KeepPrimitiveTypePattern getChar() {
+    return CHAR;
+  }
+
+  public static KeepPrimitiveTypePattern getShort() {
+    return SHORT;
+  }
+
+  public static KeepPrimitiveTypePattern getInt() {
+    return INT;
+  }
+
+  public static KeepPrimitiveTypePattern getLong() {
+    return LONG;
+  }
+
+  public static KeepPrimitiveTypePattern getFloat() {
+    return FLOAT;
+  }
+
+  public static KeepPrimitiveTypePattern getDouble() {
+    return DOUBLE;
+  }
+
+  private final char descriptor;
+
+  public KeepPrimitiveTypePattern(char descriptor) {
+    this.descriptor = descriptor;
+  }
+
+  public boolean isAny() {
+    return this == ANY;
+  }
+
+  public char getDescriptorChar() {
+    if (isAny()) {
+      throw new KeepEdgeException("No descriptor exists for 'any' primitive");
+    }
+    return descriptor;
+  }
+
+  public String getDescriptor() {
+    return Character.toString(getDescriptorChar());
+  }
+
+  public static void forEachPrimitive(Consumer<KeepPrimitiveTypePattern> fn) {
+    PRIMITIVES.values().forEach(fn);
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepTypePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepTypePattern.java
index dd56be4..b7c2b0d 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepTypePattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepTypePattern.java
@@ -3,16 +3,60 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.keepanno.ast;
 
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
 public abstract class KeepTypePattern {
 
   public static KeepTypePattern any() {
     return Any.getInstance();
   }
 
-  public static KeepTypePattern fromDescriptor(String typeDescriptor) {
-    return new Some(typeDescriptor);
+  public static KeepTypePattern fromPrimitive(KeepPrimitiveTypePattern type) {
+    return type.isAny() ? PrimitiveType.ANY : PrimitiveType.PRIMITIVES.get(type.getDescriptor());
   }
 
+  public static KeepTypePattern fromArray(KeepArrayTypePattern type) {
+    return new ArrayType(type);
+  }
+
+  public static KeepTypePattern fromClass(KeepQualifiedClassNamePattern type) {
+    return new ClassType(type);
+  }
+
+  public static KeepTypePattern fromDescriptor(String typeDescriptor) {
+    char c = typeDescriptor.charAt(0);
+    if (c == 'L') {
+      int end = typeDescriptor.length() - 1;
+      if (typeDescriptor.charAt(end) != ';') {
+        throw new KeepEdgeException("Invalid type descriptor: " + typeDescriptor);
+      }
+      return fromClass(KeepQualifiedClassNamePattern.exactFromDescriptor(typeDescriptor));
+    }
+    if (c == '[') {
+      int dim = 1;
+      while (typeDescriptor.charAt(dim) == '[') {
+        dim++;
+      }
+      KeepTypePattern baseType = fromDescriptor(typeDescriptor.substring(dim));
+      return fromArray(new KeepArrayTypePattern(baseType, dim));
+    }
+    PrimitiveType primitiveType = PrimitiveType.PRIMITIVES.get(typeDescriptor);
+    if (primitiveType != null) {
+      return primitiveType;
+    }
+    throw new KeepEdgeException("Invalid type descriptor: " + typeDescriptor);
+  }
+
+  public abstract <T> T match(
+      Supplier<T> onAny,
+      Function<KeepPrimitiveTypePattern, T> onPrimitive,
+      Function<KeepArrayTypePattern, T> onArray,
+      Function<KeepQualifiedClassNamePattern, T> onClass);
+
   public boolean isAny() {
     return false;
   }
@@ -21,44 +65,6 @@
     return null;
   }
 
-  private static class Some extends KeepTypePattern {
-
-    private final String descriptor;
-
-    private Some(String descriptor) {
-      assert descriptor != null;
-      this.descriptor = descriptor;
-    }
-
-    @Override
-    public String getDescriptor() {
-      return descriptor;
-    }
-
-    @Override
-    @SuppressWarnings("EqualsGetClass")
-    public boolean equals(Object o) {
-      if (this == o) {
-        return true;
-      }
-      if (o == null || getClass() != o.getClass()) {
-        return false;
-      }
-      Some some = (Some) o;
-      return descriptor.equals(some.descriptor);
-    }
-
-    @Override
-    public int hashCode() {
-      return descriptor.hashCode();
-    }
-
-    @Override
-    public String toString() {
-      return descriptor;
-    }
-  }
-
   private static class Any extends KeepTypePattern {
 
     private static final Any INSTANCE = new Any();
@@ -68,6 +74,15 @@
     }
 
     @Override
+    public <T> T match(
+        Supplier<T> onAny,
+        Function<KeepPrimitiveTypePattern, T> onPrimitive,
+        Function<KeepArrayTypePattern, T> onArray,
+        Function<KeepQualifiedClassNamePattern, T> onClass) {
+      return onAny.get();
+    }
+
+    @Override
     public boolean isAny() {
       return true;
     }
@@ -87,4 +102,117 @@
       return "<any>";
     }
   }
+
+  private static class PrimitiveType extends KeepTypePattern {
+
+    private static final PrimitiveType ANY = new PrimitiveType(KeepPrimitiveTypePattern.getAny());
+    private static final Map<String, PrimitiveType> PRIMITIVES = populate();
+
+    private static Map<String, PrimitiveType> populate() {
+      ImmutableMap.Builder<String, PrimitiveType> builder = ImmutableMap.builder();
+      KeepPrimitiveTypePattern.forEachPrimitive(
+          primitive -> {
+            builder.put(primitive.getDescriptor(), new PrimitiveType(primitive));
+          });
+      return builder.build();
+    }
+
+    private final KeepPrimitiveTypePattern type;
+
+    private PrimitiveType(KeepPrimitiveTypePattern type) {
+      this.type = type;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      return this == obj;
+    }
+
+    @Override
+    public int hashCode() {
+      return System.identityHashCode(this);
+    }
+
+    @Override
+    public String toString() {
+      return getDescriptor();
+    }
+
+    @Override
+    public <T> T match(
+        Supplier<T> onAny,
+        Function<KeepPrimitiveTypePattern, T> onPrimitive,
+        Function<KeepArrayTypePattern, T> onArray,
+        Function<KeepQualifiedClassNamePattern, T> onClass) {
+      return onPrimitive.apply(type);
+    }
+  }
+
+  private static class ClassType extends KeepTypePattern {
+    private final KeepQualifiedClassNamePattern type;
+
+    public ClassType(KeepQualifiedClassNamePattern type) {
+      this.type = type;
+    }
+
+    @Override
+    public <T> T match(
+        Supplier<T> onAny,
+        Function<KeepPrimitiveTypePattern, T> onPrimitive,
+        Function<KeepArrayTypePattern, T> onArray,
+        Function<KeepQualifiedClassNamePattern, T> onClass) {
+      return onClass.apply(type);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof ClassType)) {
+        return false;
+      }
+      ClassType classType = (ClassType) o;
+      return Objects.equals(type, classType.type);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(type);
+    }
+  }
+
+  private static class ArrayType extends KeepTypePattern {
+    private final KeepArrayTypePattern type;
+
+    public ArrayType(KeepArrayTypePattern type) {
+      this.type = type;
+    }
+
+    @Override
+    public <T> T match(
+        Supplier<T> onAny,
+        Function<KeepPrimitiveTypePattern, T> onPrimitive,
+        Function<KeepArrayTypePattern, T> onArray,
+        Function<KeepQualifiedClassNamePattern, T> onClass) {
+      return onArray.apply(type);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof ArrayType)) {
+        return false;
+      }
+      ArrayType arrayType = (ArrayType) o;
+      return Objects.equals(type, arrayType.type);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(type);
+    }
+  }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/ParsingContext.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/ParsingContext.java
index d732c65..0d83a25 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/ParsingContext.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/ParsingContext.java
@@ -31,10 +31,33 @@
 
   public abstract String getContextFrameAsString();
 
+  public boolean isSynthetic() {
+    return false;
+  }
+
+  private ParsingContext nonSyntheticParent() {
+    // We don't want to maintain nested property groups as they are "synthetic" and only the
+    // inner-most group is useful when diagnosing an error.
+    if (isSynthetic()) {
+      ParsingContext parent = getParentContext();
+      assert !parent.isSynthetic();
+      return parent;
+    }
+    return this;
+  }
+
   public GroupParsingContext group(String propertyGroupDescription) {
     return new GroupParsingContext(this, propertyGroupDescription);
   }
 
+  public AnnotationParsingContext annotation(String annotationDescriptor) {
+    return new AnnotationParsingContext(this, annotationDescriptor);
+  }
+
+  public PropertyParsingContext property(String propertyName) {
+    return new PropertyParsingContext(this, propertyName);
+  }
+
   public static class ClassParsingContext extends ParsingContext {
     private final String className;
 
@@ -143,7 +166,7 @@
     private final String annotationDescriptor;
 
     public AnnotationParsingContext(ParsingContext parentContext, String annotationDescriptor) {
-      this.parentContext = parentContext;
+      this.parentContext = parentContext.nonSyntheticParent();
       this.annotationDescriptor = annotationDescriptor;
     }
 
@@ -182,13 +205,7 @@
     private final String propertyGroupDescription;
 
     public GroupParsingContext(ParsingContext parentContext, String propertyGroupDescription) {
-      // We don't want to maintain nested property groups as they are "synthetic" and only the
-      // inner-most group useful for uses in diagnosing an error.
-      if (parentContext instanceof GroupParsingContext) {
-        parentContext = parentContext.getParentContext();
-      }
-      assert !(parentContext instanceof GroupParsingContext);
-      this.parentContext = parentContext;
+      this.parentContext = parentContext.nonSyntheticParent();
       this.propertyGroupDescription = propertyGroupDescription;
     }
 
@@ -197,6 +214,13 @@
     }
 
     @Override
+    public boolean isSynthetic() {
+      // The property "groups" are not actual source info and should only be used in top-level
+      // reporting.
+      return true;
+    }
+
+    @Override
     public String getHolderName() {
       return parentContext.getHolderName();
     }
@@ -216,4 +240,38 @@
       return getPropertyGroupDescription();
     }
   }
+
+  public static class PropertyParsingContext extends ParsingContext {
+    private final ParsingContext parentContext;
+    private final String propertyName;
+
+    public PropertyParsingContext(ParsingContext parentContext, String propertyName) {
+      this.parentContext = parentContext.nonSyntheticParent();
+      this.propertyName = propertyName;
+    }
+
+    public String getPropertyName() {
+      return propertyName;
+    }
+
+    @Override
+    public String getHolderName() {
+      return parentContext.getHolderName();
+    }
+
+    @Override
+    public ParsingContext getParentContext() {
+      return parentContext;
+    }
+
+    @Override
+    public String getContextType() {
+      return "property";
+    }
+
+    @Override
+    public String getContextFrameAsString() {
+      return getPropertyName();
+    }
+  }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrinter.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrinter.java
index 94694a2..518eeba 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrinter.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrinter.java
@@ -25,6 +25,7 @@
   public RulePrinter append(String str) {
     assert !str.contains("*");
     assert !str.contains("(...)");
+    assert !str.contains("%");
     return appendWithoutBackReferenceAssert(str);
   }
 
@@ -45,6 +46,10 @@
     return appendWithoutBackReferenceAssert("***");
   }
 
+  public RulePrinter appendPercent() {
+    return appendWithoutBackReferenceAssert("%");
+  }
+
   public RulePrinter appendAnyParameters() {
     return appendWithoutBackReferenceAssert("(...)");
   }
@@ -91,6 +96,11 @@
     }
 
     @Override
+    public RulePrinter appendPercent() {
+      return addBackRef("%");
+    }
+
+    @Override
     public RulePrinter appendAnyParameters() {
       // TODO(b/265892343): R8 does not yet support back reference to `...`.
       return appendWithoutBackReferenceAssert("(...)");
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java
index 5d5c357..20acbb7 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.keepanno.keeprules;
 
 import com.android.tools.r8.keepanno.ast.AccessVisibility;
+import com.android.tools.r8.keepanno.ast.KeepArrayTypePattern;
 import com.android.tools.r8.keepanno.ast.KeepClassItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepEdgeException;
 import com.android.tools.r8.keepanno.ast.KeepEdgeMetaInfo;
@@ -21,6 +22,7 @@
 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.KeepPrimitiveTypePattern;
 import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
 import com.android.tools.r8.keepanno.ast.KeepTypePattern;
 import com.android.tools.r8.keepanno.ast.KeepUnqualfiedClassNamePattern;
@@ -179,11 +181,34 @@
     return printType(builder, returnTypePattern.asType());
   }
 
-  private static RulePrinter printType(RulePrinter builder, KeepTypePattern typePattern) {
-    if (typePattern.isAny()) {
-      return builder.appendTripleStar();
+  private static RulePrinter printType(RulePrinter printer, KeepTypePattern typePattern) {
+    return typePattern.match(
+        printer::appendTripleStar,
+        primitivePattern -> printPrimitiveType(printer, primitivePattern),
+        arrayTypePattern -> printArrayType(printer, arrayTypePattern),
+        classTypePattern -> printClassName(classTypePattern, printer));
+  }
+
+  private static RulePrinter printPrimitiveType(
+      RulePrinter printer, KeepPrimitiveTypePattern primitiveTypePattern) {
+    if (primitiveTypePattern.isAny()) {
+      // Matching any primitive type uses the wildcard syntax `%`
+      return printer.appendPercent();
     }
-    return builder.append(descriptorToJavaType(typePattern.getDescriptor()));
+    return printer.append(descriptorToJavaType(primitiveTypePattern.getDescriptor()));
+  }
+
+  private static RulePrinter printArrayType(
+      RulePrinter printer, KeepArrayTypePattern arrayTypePattern) {
+    // The "any" array is simply dimension one of any type. Just assert that to be true as the
+    // general case will emit the correct syntax: ***[]
+    assert !arrayTypePattern.isAny()
+        || (arrayTypePattern.getDimensions() == 1 && arrayTypePattern.getBaseType().isAny());
+    printType(printer, arrayTypePattern.getBaseType());
+    for (int i = 0; i < arrayTypePattern.getDimensions(); i++) {
+      printer.append("[]");
+    }
+    return printer;
   }
 
   public static RulePrinter printMemberAccess(
@@ -254,7 +279,7 @@
   public static RulePrinter printClassName(
       KeepQualifiedClassNamePattern classNamePattern, RulePrinter printer) {
     if (classNamePattern.isAny()) {
-      return printer.appendStar();
+      return printer.appendDoubleStar();
     }
     printPackagePrefix(classNamePattern.getPackagePattern(), printer);
     return printSimpleClassName(classNamePattern.getNamePattern(), printer);
diff --git a/src/test/java/com/android/tools/r8/keepanno/ArrayPatternsTest.java b/src/test/java/com/android/tools/r8/keepanno/ArrayPatternsTest.java
new file mode 100644
index 0000000..a7cf54f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/ArrayPatternsTest.java
@@ -0,0 +1,136 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.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.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+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 ArrayPatternsTest extends TestBase {
+
+  static final String EXPECTED =
+      StringUtils.lines("int[] [1, 2, 3]", "int[][] [[42]]", "Integer[][][] [[[333]]]");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public ArrayPatternsTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getInputClasses())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .enableExperimentalKeepAnnotations()
+        .addProgramClasses(getInputClasses())
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkOutput);
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(TestClass.class, A.class, B.class);
+  }
+
+  private void checkOutput(CodeInspector inspector) {
+    assertThat(inspector.clazz(B.class), isPresentAndRenamed());
+    assertThat(inspector.clazz(B.class).method("void", "bar"), isAbsent());
+    assertThat(inspector.clazz(B.class).method("void", "bar", "int[]"), isPresent());
+    assertThat(inspector.clazz(B.class).method("void", "bar", "int[][]"), isPresent());
+    assertThat(inspector.clazz(B.class).method("void", "bar", "int[][][]"), isAbsent());
+    assertThat(
+        inspector.clazz(B.class).method("void", "bar", "java.lang.Integer[][][]"), isPresent());
+  }
+
+  static class A {
+
+    @UsesReflection({
+      @KeepTarget(
+          classConstant = B.class,
+          methodName = "bar",
+          methodParameterTypePatterns = {@TypePattern(constant = int[].class)}),
+      @KeepTarget(
+          classConstant = B.class,
+          methodName = "bar",
+          methodParameters = {"int[][]"}),
+      @KeepTarget(
+          classConstant = B.class,
+          methodName = "bar",
+          methodParameterTypePatterns = {@TypePattern(name = "java.lang.Integer[][][]")}),
+    })
+    public void foo() throws Exception {
+      // Invoke the first and second method.
+      B.class.getDeclaredMethod("bar", int[].class).invoke(null, (Object) new int[] {1, 2, 3});
+      B.class
+          .getDeclaredMethod("bar", int[][].class)
+          .invoke(null, (Object) new int[][] {new int[] {42}});
+      B.class
+          .getDeclaredMethod("bar", Integer[][][].class)
+          .invoke(null, (Object) new Integer[][][] {new Integer[][] {new Integer[] {333}}});
+    }
+  }
+
+  static class B {
+    public static void bar() {
+      throw new RuntimeException("UNUSED");
+    }
+
+    public static void bar(int[] value) {
+      System.out.println("int[] " + Arrays.toString(value));
+    }
+
+    public static void bar(int[][] value) {
+      System.out.println("int[][] " + Arrays.deepToString(value));
+    }
+
+    public static void bar(int[][][] value) {
+      throw new RuntimeException("UNUSED");
+    }
+
+    public static void bar(Integer[][][] value) {
+      System.out.println("Integer[][][] " + Arrays.deepToString(value));
+    }
+  }
+
+  static class TestClass {
+
+    @UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS)
+    public static void main(String[] args) throws Exception {
+      new A().foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java b/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java
index f0ea424..8a83cf9 100644
--- a/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java
@@ -53,7 +53,7 @@
             .build();
     assertEquals(
         StringUtils.unixLines(
-            "-keep class * { void finalize(); }", "-keepclassmembers class * { *; }"),
+            "-keep class ** { void finalize(); }", "-keepclassmembers class ** { *; }"),
         extract(edge));
   }
 
@@ -83,8 +83,8 @@
     // targeted members.
     assertEquals(
         StringUtils.unixLines(
-            "-keep,allow" + allows + " class * { void finalize(); }",
-            "-keepclassmembers,allow" + allows + " class * { *; }"),
+            "-keep,allow" + allows + " class ** { void finalize(); }",
+            "-keepclassmembers,allow" + allows + " class ** { *; }"),
         extract(edge));
   }
 
@@ -110,8 +110,8 @@
     // Allow is just the ordered list of options.
     assertEquals(
         StringUtils.unixLines(
-            "-keep,allowshrinking,allowobfuscation class * { void finalize(); }",
-            "-keepclassmembers,allowshrinking,allowobfuscation class * { *; }"),
+            "-keep,allowshrinking,allowobfuscation class ** { void finalize(); }",
+            "-keepclassmembers,allowshrinking,allowobfuscation class ** { *; }"),
         extract(edge));
   }
 
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
index 3468b4c..699ddd6 100644
--- a/src/test/java/com/android/tools/r8/keepanno/classpatterns/ClassNamePatternsTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/classpatterns/ClassNamePatternsTest.java
@@ -30,7 +30,19 @@
   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_ALL =
+      StringUtils.lines(
+          "pkg1.A",
+          "pkg1.A: pkg1.A",
+          "pkg1.B",
+          "pkg1.B: pkg1.B",
+          "pkg2.A",
+          "pkg2.A: pkg2.A",
+          "pkg2.B",
+          "pkg2.B: pkg2.B");
+
+  static final String EXPECTED_ALL_NON_VOID =
+      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");
@@ -71,6 +83,11 @@
   }
 
   @Test
+  public void testAllNoVoidR8() throws Exception {
+    runTestR8(TestAllNoVoid.class, EXPECTED_ALL_NON_VOID);
+  }
+
+  @Test
   public void testPkgR8() throws Exception {
     runTestR8(TestPkg.class, EXPECTED_PKG);
   }
@@ -85,6 +102,11 @@
     runTestR8(TestSingle.class, EXPECTED_SINGLE);
   }
 
+  @Test
+  public void testSingleNonExactR8() throws Exception {
+    runTestR8(TestSingleWithNonExactReturnTypeClassPattern.class, EXPECTED_SINGLE);
+  }
+
   public List<Class<?>> getBaseInputClasses() {
     return ImmutableList.of(Util.class, A1, B1, A2, B2);
   }
@@ -97,6 +119,7 @@
           try {
             Class<?> clazz = Class.forName(type);
             System.out.println(clazz.getDeclaredMethod("foo").invoke(null));
+            clazz.getDeclaredMethod("foo", String.class).invoke(null, pkg + "." + name);
           } catch (ClassNotFoundException ignored) {
           } catch (IllegalAccessException ignored) {
           } catch (InvocationTargetException ignored) {
@@ -115,7 +138,8 @@
           // The empty class pattern is equivalent to "any class".
           classNamePattern = @ClassNamePattern(),
           methodName = "foo",
-          methodReturnTypeConstant = String.class)
+          // The empty type pattern used in a return-type context will match 'void'.
+          methodReturnTypePattern = @TypePattern())
     })
     public void foo() throws Exception {
       Util.lookupClassesAndInvokeMethods();
@@ -127,6 +151,25 @@
     }
   }
 
+  static class TestAllNoVoid {
+
+    @UsesReflection({
+      @KeepTarget(
+          kind = KeepItemKind.CLASS_AND_METHODS,
+          methodName = "foo",
+          // Matching any class does not include 'void'.
+          methodReturnTypePattern = @TypePattern(classNamePattern = @ClassNamePattern()))
+    })
+    public void foo() throws Exception {
+      Util.lookupClassesAndInvokeMethods();
+    }
+
+    @UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS)
+    public static void main(String[] args) throws Exception {
+      new TestAllNoVoid().foo();
+    }
+  }
+
   static class TestPkg {
 
     @UsesReflection({
@@ -189,4 +232,26 @@
       new TestSingle().foo();
     }
   }
+
+  static class TestSingleWithNonExactReturnTypeClassPattern {
+
+    @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(simpleName = "String"))))
+    public void foo() throws Exception {
+      Util.lookupClassesAndInvokeMethods();
+    }
+
+    @UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS)
+    public static void main(String[] args) throws Exception {
+      new TestSingleWithNonExactReturnTypeClassPattern().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
index eab0e5f..377601c 100644
--- 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
@@ -9,4 +9,8 @@
   public static String foo() {
     return "pkg1.A";
   }
+
+  public static void foo(String arg) {
+    System.out.println("pkg1.A: " + arg);
+  }
 }
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
index edeb6b8..b37f208 100644
--- 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
@@ -9,4 +9,8 @@
   public static String foo() {
     return "pkg1.B";
   }
+
+  public static void foo(String arg) {
+    System.out.println("pkg1.B: " + arg);
+  }
 }
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
index ca0667e..1670103 100644
--- 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
@@ -9,4 +9,8 @@
   public static String foo() {
     return "pkg2.A";
   }
+
+  public static void foo(String arg) {
+    System.out.println("pkg2.A: " + arg);
+  }
 }
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
index c98e746..ab18f98 100644
--- 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
@@ -9,4 +9,8 @@
   public static String foo() {
     return "pkg2.B";
   }
+
+  public static void foo(String arg) {
+    System.out.println("pkg2.B: " + arg);
+  }
 }