[KeepAnno] Add support for method return type and parameters.

Bug: b/248408342
Change-Id: Ib6914bdb27f8116ac64acf679cf0ff5e4a1185ea
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstants.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstants.java
index 260c31c..a9bd519 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstants.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstants.java
@@ -37,8 +37,16 @@
   // Implicit hidden item which is "super type" of Condition and Target.
   public static final class Item {
     public static final String classConstant = "classConstant";
+
     public static final String methodName = "methodName";
+    public static final String methodReturnType = "methodReturnType";
+    public static final String methodParameters = "methodParameters";
+    public static final String methodNameDefaultValue = "";
+    public static final String methodReturnTypeDefaultValue = "";
+    public static final String[] methodParametersDefaultValue = new String[] {"<any>"};
+
     public static final String fieldName = "fieldName";
+    public static final String fieldNameDefaultValue = "";
   }
 
   public static final class Condition {
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
index f87e32c..1b9fe64 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
@@ -17,5 +17,9 @@
 
   String methodName() default "";
 
+  String methodReturnType() default "";
+
+  String[] methodParameters() default {"<any>"};
+
   String fieldName() default "";
 }
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 3545a25..4efa5e8 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
@@ -17,12 +17,19 @@
 import com.android.tools.r8.keepanno.ast.KeepItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepItemPattern.Builder;
 import com.android.tools.r8.keepanno.ast.KeepMethodNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodParametersPattern;
 import com.android.tools.r8.keepanno.ast.KeepMethodPattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodReturnTypePattern;
 import com.android.tools.r8.keepanno.ast.KeepPreconditions;
 import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
 import com.android.tools.r8.keepanno.ast.KeepTarget;
+import com.android.tools.r8.keepanno.ast.KeepTypePattern;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
+import java.util.function.Consumer;
 import org.objectweb.asm.AnnotationVisitor;
 import org.objectweb.asm.ClassReader;
 import org.objectweb.asm.ClassVisitor;
@@ -115,14 +122,24 @@
     }
 
     private KeepItemPattern createItemContext() {
-      Type returnType = Type.getReturnType(methodDescriptor);
+      String returnTypeDescriptor = Type.getReturnType(methodDescriptor).getDescriptor();
       Type[] argumentTypes = Type.getArgumentTypes(methodDescriptor);
-      // TODO(b/248408342): Defaults are "any", support setting actual return type and params.
+      KeepMethodParametersPattern.Builder builder = KeepMethodParametersPattern.builder();
+      for (Type type : argumentTypes) {
+        builder.addParameterTypePattern(KeepTypePattern.fromDescriptor(type.getDescriptor()));
+      }
+      KeepMethodReturnTypePattern returnTypePattern =
+          "V".equals(returnTypeDescriptor)
+              ? KeepMethodReturnTypePattern.voidType()
+              : KeepMethodReturnTypePattern.fromType(
+                  KeepTypePattern.fromDescriptor(returnTypeDescriptor));
       return KeepItemPattern.builder()
           .setClassPattern(KeepQualifiedClassNamePattern.exact(className))
           .setMemberPattern(
               KeepMethodPattern.builder()
                   .setNamePattern(KeepMethodNamePattern.exact(methodName))
+                  .setReturnTypePattern(returnTypePattern)
+                  .setParametersPattern(builder.build())
                   .build())
           .build();
     }
@@ -320,13 +337,33 @@
     private final Parent<KeepItemPattern> parent;
 
     private KeepQualifiedClassNamePattern classNamePattern = null;
-    private KeepMethodNamePattern methodName = null;
-    private KeepFieldNamePattern fieldName = null;
+    private KeepMethodPattern.Builder lazyMethodBuilder = null;
+    private KeepFieldPattern.Builder lazyFieldBuilder = null;
 
     public KeepItemVisitorBase(Parent<KeepItemPattern> parent) {
       this.parent = parent;
     }
 
+    private KeepMethodPattern.Builder methodBuilder() {
+      if (lazyFieldBuilder != null) {
+        throw new KeepEdgeException("Cannot define both a field and a method pattern");
+      }
+      if (lazyMethodBuilder == null) {
+        lazyMethodBuilder = KeepMethodPattern.builder();
+      }
+      return lazyMethodBuilder;
+    }
+
+    private KeepFieldPattern.Builder fieldBuilder() {
+      if (lazyMethodBuilder != null) {
+        throw new KeepEdgeException("Cannot define both a field and a method pattern");
+      }
+      if (lazyFieldBuilder == null) {
+        lazyFieldBuilder = KeepFieldPattern.builder();
+      }
+      return lazyFieldBuilder;
+    }
+
     @Override
     public void visit(String name, Object value) {
       if (name.equals(Item.classConstant) && value instanceof Type) {
@@ -334,36 +371,90 @@
         return;
       }
       if (name.equals(Item.methodName) && value instanceof String) {
-        methodName = KeepMethodNamePattern.exact((String) value);
+        String methodName = (String) value;
+        if (!Item.methodNameDefaultValue.equals(methodName)) {
+          methodBuilder().setNamePattern(KeepMethodNamePattern.exact(methodName));
+        }
+        return;
+      }
+      if (name.equals(Item.methodReturnType) && value instanceof String) {
+        String returnType = (String) value;
+        if (!Item.methodReturnTypeDefaultValue.equals(returnType)) {
+          methodBuilder()
+              .setReturnTypePattern(KeepEdgeReaderUtils.methodReturnTypeFromString(returnType));
+        }
         return;
       }
       if (name.equals(Item.fieldName) && value instanceof String) {
-        fieldName = KeepFieldNamePattern.exact((String) value);
+        String fieldName = (String) value;
+        if (!Item.fieldNameDefaultValue.equals(fieldName)) {
+          fieldBuilder().setNamePattern(KeepFieldNamePattern.exact(fieldName));
+        }
         return;
       }
       super.visit(name, value);
     }
 
     @Override
+    public AnnotationVisitor visitArray(String name) {
+      if (name.equals(Item.methodParameters)) {
+        return new StringArrayVisitor(
+            params -> {
+              if (Arrays.asList(Item.methodParametersDefaultValue).equals(params)) {
+                return;
+              }
+              KeepMethodParametersPattern.Builder builder = KeepMethodParametersPattern.builder();
+              for (String param : params) {
+                builder.addParameterTypePattern(KeepEdgeReaderUtils.typePatternFromString(param));
+              }
+              methodBuilder().setParametersPattern(builder.build());
+            });
+      }
+      return super.visitArray(name);
+    }
+
+    @Override
     public void visitEnd() {
+      assert lazyMethodBuilder == null || lazyFieldBuilder == null;
       Builder itemBuilder = KeepItemPattern.builder();
       if (classNamePattern != null) {
         itemBuilder.setClassPattern(classNamePattern);
       }
-      if (methodName != null && fieldName != null) {
-        throw new KeepEdgeException("Cannot define both a field and a method pattern.");
+      if (lazyMethodBuilder != null) {
+        itemBuilder.setMemberPattern(lazyMethodBuilder.build());
       }
-      if (methodName != null) {
-        itemBuilder.setMemberPattern(
-            KeepMethodPattern.builder().setNamePattern(methodName).build());
-      }
-      if (fieldName != null) {
-        itemBuilder.setMemberPattern(KeepFieldPattern.builder().setNamePattern(fieldName).build());
+      if (lazyFieldBuilder != null) {
+        itemBuilder.setMemberPattern(lazyFieldBuilder.build());
       }
       parent.accept(itemBuilder.build());
     }
   }
 
+  private static class StringArrayVisitor extends AnnotationVisitorBase {
+
+    private final Consumer<List<String>> fn;
+    private final List<String> strings = new ArrayList<>();
+
+    public StringArrayVisitor(Consumer<List<String>> fn) {
+      this.fn = fn;
+    }
+
+    @Override
+    public void visit(String name, Object value) {
+      if (value instanceof String) {
+        strings.add((String) value);
+      } else {
+        super.visit(name, value);
+      }
+    }
+
+    @Override
+    public void visitEnd() {
+      super.visitEnd();
+      fn.accept(strings);
+    }
+  }
+
   private static class KeepTargetVisitor extends KeepItemVisitorBase {
 
     public KeepTargetVisitor(Parent<KeepTarget> parent) {
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
new file mode 100644
index 0000000..e18bf1e
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReaderUtils.java
@@ -0,0 +1,69 @@
+// Copyright (c) 2022, 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 com.android.tools.r8.keepanno.ast.KeepMethodReturnTypePattern;
+import com.android.tools.r8.keepanno.ast.KeepTypePattern;
+
+/**
+ * Utilities for mapping the syntax used in annotations to the keep-edge AST.
+ *
+ * <p>The AST explicitly avoids interpreting type strings as they are potentially ambiguous. These
+ * utilities define the mappings from such syntax strings into the AST.
+ */
+public class KeepEdgeReaderUtils {
+
+  public static KeepTypePattern typePatternFromString(String string) {
+    if (string.equals("<any>")) {
+      return KeepTypePattern.any();
+    }
+    return KeepTypePattern.fromDescriptor(javaTypeToDescriptor(string));
+  }
+
+  public static String javaTypeToDescriptor(String type) {
+    switch (type) {
+      case "boolean":
+        return "Z";
+      case "byte":
+        return "B";
+      case "short":
+        return "S";
+      case "int":
+        return "I";
+      case "long":
+        return "J";
+      case "float":
+        return "F";
+      case "double":
+        return "D";
+      default:
+        {
+          StringBuilder builder = new StringBuilder(type.length());
+          int i = type.length() - 1;
+          while (type.charAt(i) == ']') {
+            if (type.charAt(--i) != '[') {
+              throw new KeepEdgeException("Invalid type: " + type);
+            }
+            builder.append('[');
+            --i;
+          }
+          builder.append('L');
+          for (int j = 0; j <= i; j++) {
+            char c = type.charAt(j);
+            builder.append(c == '.' ? '/' : c);
+          }
+          builder.append(';');
+          return builder.toString();
+        }
+    }
+  }
+
+  public static KeepMethodReturnTypePattern methodReturnTypeFromString(String returnType) {
+    if ("void".equals(returnType)) {
+      return KeepMethodReturnTypePattern.voidType();
+    }
+    return KeepMethodReturnTypePattern.fromType(typePatternFromString(returnType));
+  }
+}
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 b2da25a..e1d6d4e 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
@@ -33,7 +33,7 @@
  *     ::= any
  *       | class QUALIFIED_CLASS_NAME_PATTERN extends EXTENDS_PATTERN { MEMBER_PATTERN }
  *
- *   TYPE_PATTERN ::= any
+ *   TYPE_PATTERN ::= any | exact type-descriptor
  *   PACKAGE_PATTERN ::= any | exact package-name
  *   QUALIFIED_CLASS_NAME_PATTERN ::= any | PACKAGE_PATTERN | UNQUALIFIED_CLASS_NAME_PATTERN
  *   UNQUALIFIED_CLASS_NAME_PATTERN ::= any | exact simple-class-name
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodParametersPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodParametersPattern.java
index 29442ff..73f8669 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodParametersPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodParametersPattern.java
@@ -3,11 +3,16 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.keepanno.ast;
 
+import com.google.common.collect.ImmutableList;
 import java.util.Collections;
 import java.util.List;
 
 public abstract class KeepMethodParametersPattern {
 
+  public static Builder builder() {
+    return new Builder();
+  }
+
   public static KeepMethodParametersPattern any() {
     return Any.getInstance();
   }
@@ -30,6 +35,25 @@
     return null;
   }
 
+  public static class Builder {
+    ImmutableList.Builder<KeepTypePattern> parameterPatterns = ImmutableList.builder();
+
+    private Builder() {}
+
+    public Builder addParameterTypePattern(KeepTypePattern typePattern) {
+      parameterPatterns.add(typePattern);
+      return this;
+    }
+
+    public KeepMethodParametersPattern build() {
+      List<KeepTypePattern> list = parameterPatterns.build();
+      if (list.isEmpty()) {
+        return Some.EMPTY_INSTANCE;
+      }
+      return new Some(list);
+    }
+  }
+
   private static class Some extends KeepMethodParametersPattern {
 
     private static final Some EMPTY_INSTANCE = new Some(Collections.emptyList());
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodPattern.java
index e00fedd..f84a4b1 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodPattern.java
@@ -35,11 +35,15 @@
       return self();
     }
 
-    public Builder setReturnTypeVoid() {
-      returnTypePattern = KeepMethodReturnTypePattern.voidType();
+    public Builder setReturnTypePattern(KeepMethodReturnTypePattern returnTypePattern) {
+      this.returnTypePattern = returnTypePattern;
       return self();
     }
 
+    public Builder setReturnTypeVoid() {
+      return setReturnTypePattern(KeepMethodReturnTypePattern.voidType());
+    }
+
     public Builder setParametersPattern(KeepMethodParametersPattern parametersPattern) {
       this.parametersPattern = parametersPattern;
       return self();
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodReturnTypePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodReturnTypePattern.java
index ccfc183..807b1bc 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodReturnTypePattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodReturnTypePattern.java
@@ -14,6 +14,10 @@
     return VoidType.getInstance();
   }
 
+  public static KeepMethodReturnTypePattern fromType(KeepTypePattern typePattern) {
+    return typePattern.isAny() ? any() : new SomeType(typePattern);
+  }
+
   public boolean isAny() {
     return isType() && asType().isAny();
   }
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 c7c24b5..872ad07 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
@@ -9,7 +9,57 @@
     return Any.getInstance();
   }
 
+  public static KeepTypePattern fromDescriptor(String typeDescriptor) {
+    return new Some(typeDescriptor);
+  }
+
+  public boolean isAny() {
+    return false;
+  }
+
+  public String getDescriptor() {
+    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
+    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();
 
     public static Any getInstance() {
@@ -33,9 +83,7 @@
 
     @Override
     public String toString() {
-      return "*";
+      return "<any>";
     }
   }
-
-  public abstract boolean isAny();
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
index 6781a22..286c07a 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.keepanno.ast.KeepConsequences;
 import com.android.tools.r8.keepanno.ast.KeepEdge;
+import com.android.tools.r8.keepanno.ast.KeepEdgeException;
 import com.android.tools.r8.keepanno.ast.KeepFieldAccessPattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldNamePattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
@@ -157,7 +158,7 @@
         .append('(')
         .append(
             parametersPattern.asList().stream()
-                .map(Object::toString)
+                .map(KeepRuleExtractor::getTypePatternString)
                 .collect(Collectors.joining(", ")))
         .append(')');
   }
@@ -185,10 +186,7 @@
   }
 
   private static StringBuilder printType(StringBuilder builder, KeepTypePattern typePattern) {
-    if (typePattern.isAny()) {
-      return builder.append("***");
-    }
-    throw new Unimplemented();
+    return builder.append(getTypePatternString(typePattern));
   }
 
   private static StringBuilder printAccess(
@@ -256,6 +254,70 @@
     }
   }
 
+  private static String getTypePatternString(KeepTypePattern typePattern) {
+    if (typePattern.isAny()) {
+      return "***";
+    }
+    return descriptorToJavaType(typePattern.getDescriptor());
+  }
+
+  private static String descriptorToJavaType(String descriptor) {
+    if (descriptor.isEmpty()) {
+      throw new KeepEdgeException("Invalid empty type descriptor");
+    }
+    if (descriptor.length() == 1) {
+      return primitiveDescriptorToJavaType(descriptor.charAt(0));
+    }
+    if (descriptor.charAt(0) == '[') {
+      return arrayDescriptorToJavaType(descriptor);
+    }
+    return classDescriptorToJavaType(descriptor);
+  }
+
+  private static String primitiveDescriptorToJavaType(char descriptor) {
+    switch (descriptor) {
+      case 'Z':
+        return "boolean";
+      case 'B':
+        return "byte";
+      case 'S':
+        return "short";
+      case 'I':
+        return "int";
+      case 'J':
+        return "long";
+      case 'F':
+        return "float";
+      case 'D':
+        return "double";
+      default:
+        throw new KeepEdgeException("Invalid primitive descriptor: " + descriptor);
+    }
+  }
+
+  private static String classDescriptorToJavaType(String descriptor) {
+    int last = descriptor.length() - 1;
+    if (descriptor.charAt(0) != 'L' || descriptor.charAt(last) != ';') {
+      throw new KeepEdgeException("Invalid class descriptor: " + descriptor);
+    }
+    return descriptor.substring(1, last).replace('/', '.');
+  }
+
+  private static String arrayDescriptorToJavaType(String descriptor) {
+    for (int i = 0; i < descriptor.length(); i++) {
+      char c = descriptor.charAt(i);
+      if (c != '[') {
+        StringBuilder builder = new StringBuilder();
+        builder.append(descriptorToJavaType(descriptor.substring(i)));
+        for (int j = 0; j < i; j++) {
+          builder.append("[]");
+        }
+        return builder.toString();
+      }
+    }
+    throw new KeepEdgeException("Invalid array descriptor: " + descriptor);
+  }
+
   private static class ItemRule {
     private final KeepTarget target;
     private final KeepOptions options;
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationTest.java
index 9bee408..f9d98e3 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationTest.java
@@ -3,6 +3,7 @@
 // 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 org.hamcrest.MatcherAssert.assertThat;
 
@@ -53,13 +54,14 @@
         .addKeepRules(rules)
         .addKeepMainRule(TestClass.class)
         .setMinApi(parameters.getApiLevel())
+        .allowUnusedProguardConfigurationRules()
         .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutput(EXPECTED)
         .inspect(this::checkOutput);
   }
 
   public List<Class<?>> getInputClasses() {
-    return ImmutableList.of(TestClass.class, A.class, B.class);
+    return ImmutableList.of(TestClass.class, A.class, B.class, C.class);
   }
 
   public List<byte[]> getInputClassesWithoutAnnotations() throws Exception {
@@ -83,7 +85,9 @@
   private void checkOutput(CodeInspector inspector) {
     assertThat(inspector.clazz(A.class), isPresent());
     assertThat(inspector.clazz(B.class), isPresent());
-    assertThat(inspector.clazz(B.class).uniqueMethodWithOriginalName("bar"), isPresent());
+    assertThat(inspector.clazz(C.class), isAbsent());
+    assertThat(inspector.clazz(B.class).method("void", "bar"), isPresent());
+    assertThat(inspector.clazz(B.class).method("void", "bar", "int"), isAbsent());
   }
 
   static class A {
@@ -94,18 +98,36 @@
       // Ensure that the class B remains as we are looking it up by reflected name.
       @KeepTarget(classConstant = B.class),
       // Ensure the method 'bar' remains as we are invoking it by reflected name.
-      @KeepTarget(classConstant = B.class, methodName = "bar")
+      @KeepTarget(
+          classConstant = B.class,
+          methodName = "bar",
+          methodParameters = {},
+          methodReturnType = "void")
     })
     public void foo() throws Exception {
       Class<?> clazz = Class.forName(A.class.getTypeName().replace("$A", "$B"));
       clazz.getDeclaredMethod("bar").invoke(clazz);
     }
+
+    // This annotation is not active as its implicit precondition "void A.foo(int)" is not used.
+    @UsesReflection({@KeepTarget(classConstant = C.class)})
+    public void foo(int unused) {
+      // Unused.
+    }
   }
 
   static class B {
     public static void bar() {
       System.out.println("Hello, world");
     }
+
+    public static void bar(int ignore) {
+      throw new RuntimeException("UNUSED");
+    }
+  }
+
+  static class C {
+    // Unused.
   }
 
   static class TestClass {