[keepanno] Initial implementation of support for @UsesReflectiontoAccessMethod

Bug: b/392865072
Change-Id: Ibe8aa5b2a71a2d1aeb88e8448ca25c9905bde68a
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 6b0c939..023c844 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
@@ -21,6 +21,7 @@
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.MethodAccess;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Target;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.UsedByReflection;
+import com.android.tools.r8.keepanno.ast.AnnotationConstants.UsesReflectionToAccessMethod;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.UsesReflectionToConstruct;
 import com.android.tools.r8.keepanno.ast.KeepAnnotationPattern;
 import com.android.tools.r8.keepanno.ast.KeepBindingReference;
@@ -107,12 +108,15 @@
     if (AnnotationConstants.Edge.isDescriptor(descriptor)
         || AnnotationConstants.UsesReflection.isDescriptor(descriptor)
         || AnnotationConstants.UsesReflectionToConstruct.isDescriptor(descriptor)
+        || AnnotationConstants.UsesReflectionToAccessMethod.isDescriptor(descriptor)
         || AnnotationConstants.ForApi.isDescriptor(descriptor)
         || AnnotationConstants.UsedByReflection.isDescriptor(descriptor)
         || AnnotationConstants.UsedByNative.isDescriptor(descriptor)
         || AnnotationConstants.CheckRemoved.isDescriptor(descriptor)
         || AnnotationConstants.CheckOptimizedOut.isDescriptor(descriptor)
         || AnnotationConstants.UsesReflectionToConstruct.isKotlinRepeatableContainerDescriptor(
+            descriptor)
+        || AnnotationConstants.UsesReflectionToAccessMethod.isKotlinRepeatableContainerDescriptor(
             descriptor)) {
       return true;
     }
@@ -377,6 +381,16 @@
                     .setClassNamePattern(KeepQualifiedClassNamePattern.exact(className))
                     .build());
       }
+      if (AnnotationConstants.UsesReflectionToAccessMethod.isDescriptor(descriptor)) {
+        return new UsesReflectionToAccessMethodVisitor(
+            parsingContext,
+            parent::accept,
+            setContext,
+            bindingsHelper ->
+                KeepClassItemPattern.builder()
+                    .setClassNamePattern(KeepQualifiedClassNamePattern.exact(className))
+                    .build());
+      }
       if (ForApi.isDescriptor(descriptor)) {
         return new ForApiClassVisitor(parsingContext, parent::accept, setContext, className);
       }
@@ -536,6 +550,23 @@
             bindingsHelper ->
                 createMethodItemContext(className, methodName, methodDescriptor, bindingsHelper));
       }
+      if (AnnotationConstants.UsesReflectionToAccessMethod.isDescriptor(descriptor)) {
+        return new UsesReflectionToAccessMethodVisitor(
+            parsingContext,
+            parent::accept,
+            setContext,
+            bindingsHelper ->
+                createMethodItemContext(className, methodName, methodDescriptor, bindingsHelper));
+      }
+      if (AnnotationConstants.UsesReflectionToAccessMethod.isKotlinRepeatableContainerDescriptor(
+          descriptor)) {
+        return new UsesReflectionToAccessMethodContainerVisitor(
+            parsingContext,
+            parent::accept,
+            setContext,
+            bindingsHelper ->
+                createMethodItemContext(className, methodName, methodDescriptor, bindingsHelper));
+      }
       if (AnnotationConstants.ForApi.isDescriptor(descriptor)) {
         return new ForApiMemberVisitor(
             parsingContext,
@@ -1638,6 +1669,235 @@
     }
   }
 
+  private static class UsesReflectionToAccessMethodContainerVisitor extends AnnotationVisitorBase {
+
+    private final AnnotationParsingContext parsingContext;
+    private final Parent<KeepEdge> parent;
+    Consumer<KeepEdgeMetaInfo.Builder> addContext;
+    Function<UserBindingsHelper, KeepItemPattern> contextBuilder;
+
+    UsesReflectionToAccessMethodContainerVisitor(
+        AnnotationParsingContext parsingContext,
+        Parent<KeepEdge> parent,
+        Consumer<KeepEdgeMetaInfo.Builder> addContext,
+        Function<UserBindingsHelper, KeepItemPattern> contextBuilder) {
+      super(parsingContext);
+      this.parsingContext = parsingContext;
+      this.parent = parent;
+      this.addContext = addContext;
+      this.contextBuilder = contextBuilder;
+    }
+
+    @Override
+    public AnnotationVisitor visitArray(String name) {
+      if (name.equals("value")) {
+        return new UsesReflectionToAccessMethodContainerElementVisitor(
+            parsingContext, parent, addContext, contextBuilder);
+      }
+      return super.visitArray(name);
+    }
+
+    @Override
+    public void visit(String name, Object value) {
+      super.visit(name, value);
+    }
+
+    @Override
+    public void visitEnd() {
+      super.visitEnd();
+    }
+  }
+
+  private static class UsesReflectionToAccessMethodContainerElementVisitor
+      extends AnnotationVisitorBase {
+    private final AnnotationParsingContext parsingContext;
+    private final Parent<KeepEdge> parent;
+    Consumer<KeepEdgeMetaInfo.Builder> addContext;
+    Function<UserBindingsHelper, KeepItemPattern> contextBuilder;
+
+    public UsesReflectionToAccessMethodContainerElementVisitor(
+        AnnotationParsingContext parsingContext,
+        Parent<KeepEdge> parent,
+        Consumer<KeepEdgeMetaInfo.Builder> addContext,
+        Function<UserBindingsHelper, KeepItemPattern> contextBuilder) {
+      super(parsingContext);
+      this.parsingContext = parsingContext;
+      this.parent = parent;
+      this.addContext = addContext;
+      this.contextBuilder = contextBuilder;
+    }
+
+    @Override
+    public AnnotationVisitor visitAnnotation(String name, String descriptor) {
+      assert name == null;
+      if (AnnotationConstants.UsesReflectionToAccessMethod.isDescriptor(descriptor)) {
+        return new UsesReflectionToAccessMethodVisitor(
+            parsingContext, parent, addContext, contextBuilder);
+      }
+      return super.visitAnnotation(name, descriptor);
+    }
+
+    @Override
+    public void visit(String name, Object value) {
+      super.visit(name, value);
+    }
+  }
+
+  private static class UsesReflectionToAccessMethodVisitor extends AnnotationVisitorBase {
+
+    private final ParsingContext parsingContext;
+    private final Parent<KeepEdge> parent;
+    private final KeepEdge.Builder builder = KeepEdge.builder();
+    private final KeepPreconditions.Builder preconditions = KeepPreconditions.builder();
+    private final KeepEdgeMetaInfo.Builder metaInfoBuilder = KeepEdgeMetaInfo.builder();
+    private KeepMethodParametersPattern parameters = KeepMethodParametersPattern.any();
+    private final UserBindingsHelper bindingsHelper = new UserBindingsHelper();
+
+    private KeepQualifiedClassNamePattern qualifiedName;
+    private KeepMethodNamePattern methodName;
+    private KeepMethodReturnTypePattern returnType = KeepMethodReturnTypePattern.any();
+
+    UsesReflectionToAccessMethodVisitor(
+        AnnotationParsingContext parsingContext,
+        Parent<KeepEdge> parent,
+        Consumer<KeepEdgeMetaInfo.Builder> addContext,
+        Function<UserBindingsHelper, KeepItemPattern> contextBuilder) {
+      super(parsingContext);
+      this.parsingContext = parsingContext;
+      this.parent = parent;
+      KeepItemPattern context = contextBuilder.apply(bindingsHelper);
+      KeepBindingReference contextBinding =
+          bindingsHelper.defineFreshItemBinding("CONTEXT", context);
+      preconditions.addCondition(KeepCondition.builder().setItemReference(contextBinding).build());
+      addContext.accept(metaInfoBuilder);
+    }
+
+    @Override
+    public void visit(String name, Object value) {
+      if (name.equals(UsesReflectionToAccessMethod.classConstant) && value instanceof Type) {
+        qualifiedName =
+            KeepQualifiedClassNamePattern.exactFromDescriptor(((Type) value).getDescriptor());
+        return;
+      }
+      if (name.equals(AnnotationConstants.UsesReflectionToAccessMethod.className)
+          && value instanceof String) {
+        qualifiedName = KeepQualifiedClassNamePattern.exact((String) value);
+        return;
+      }
+      if (name.equals(UsesReflectionToAccessMethod.methodName) && value instanceof String) {
+        methodName = KeepMethodNamePattern.exact((String) value);
+        return;
+      }
+      if (name.equals(UsesReflectionToAccessMethod.returnType) && value instanceof Type) {
+        returnType =
+            KeepMethodReturnTypePattern.fromType(
+                KeepTypePattern.fromDescriptor(((Type) value).getDescriptor()));
+        return;
+      }
+      if (name.equals(UsesReflectionToAccessMethod.returnTypeName) && value instanceof String) {
+        returnType =
+            KeepMethodReturnTypePattern.fromType(
+                KeepTypePattern.fromClass(
+                    KeepClassPattern.fromName(
+                        KeepQualifiedClassNamePattern.exact((String) value))));
+        return;
+      }
+      super.visit(name, value);
+    }
+
+    @Override
+    public AnnotationVisitor visitArray(String name) {
+      PropertyParsingContext propertyParsingContext = parsingContext.property(name);
+      if (name.equals(UsesReflectionToAccessMethod.parameterTypes)) {
+        return new ParametersClassVisitor(
+            propertyParsingContext, parameters -> this.parameters = parameters);
+      }
+      if (name.equals(UsesReflectionToAccessMethod.parameterTypeNames)) {
+        return new ParametersClassNamesVisitor(
+            propertyParsingContext, parameters -> this.parameters = parameters);
+      }
+      return super.visitArray(name);
+    }
+
+    @Override
+    public void visitEnd() {
+      String kotlinMetadataDescriptor = "Lkotlin/Metadata;";
+
+      KeepClassBindingReference classBinding =
+          bindingsHelper.defineFreshClassBinding(
+              KeepClassItemPattern.builder()
+                  .setClassPattern(
+                      KeepClassPattern.builder().setClassNamePattern(qualifiedName).build())
+                  .build());
+      KeepMemberBindingReference memberBinding =
+          bindingsHelper.defineFreshMemberBinding(
+              KeepMemberItemPattern.builder()
+                  .setClassReference(classBinding)
+                  .setMemberPattern(
+                      KeepMethodPattern.builder()
+                          .setNamePattern(methodName)
+                          .setParametersPattern(parameters)
+                          .setReturnTypePattern(returnType)
+                          .build())
+                  .build());
+
+      KeepClassBindingReference kotlinMetadataBinding =
+          bindingsHelper.defineFreshClassBinding(
+              KeepClassItemPattern.builder()
+                  .setClassPattern(KeepClassPattern.exactFromDescriptor(kotlinMetadataDescriptor))
+                  .build());
+      KeepMemberBindingReference kotlinMetadataMembersBinding =
+          bindingsHelper.defineFreshMemberBinding(
+              KeepMemberItemPattern.builder()
+                  .setClassReference(kotlinMetadataBinding)
+                  .setMemberPattern(KeepMemberPattern.allMembers())
+                  .build());
+
+      Annotation keepConstraintKotlinMetadataAnnotation =
+          KeepConstraint.annotation(
+              KeepAnnotationPattern.builder()
+                  .setNamePattern(
+                      KeepQualifiedClassNamePattern.exactFromDescriptor(kotlinMetadataDescriptor))
+                  .addRetentionPolicy(RetentionPolicy.RUNTIME)
+                  .build());
+
+      KeepConsequences.Builder consequencesBuilder =
+          KeepConsequences.builder()
+              .addTarget(
+                  KeepTarget.builder()
+                      .setItemReference(classBinding)
+                      .setConstraints(
+                          KeepConstraints.defaultAdditions(
+                              KeepConstraints.builder()
+                                  .add(keepConstraintKotlinMetadataAnnotation)
+                                  .build()))
+                      .build())
+              .addTarget(
+                  KeepTarget.builder()
+                      .setItemReference(memberBinding)
+                      // Keeping the kotlin.Metadata annotation on the members is not really needed,
+                      // as the annotation is only supported on classes. However, having it here
+                      // makes the keep rule extraction generate more compact rules.
+                      .setConstraints(
+                          KeepConstraints.defaultAdditions(
+                              KeepConstraints.builder()
+                                  .add(keepConstraintKotlinMetadataAnnotation)
+                                  .build()))
+                      .build())
+              .addTarget(KeepTarget.builder().setItemReference(kotlinMetadataBinding).build())
+              .addTarget(
+                  KeepTarget.builder().setItemReference(kotlinMetadataMembersBinding).build());
+
+      parent.accept(
+          builder
+              .setMetaInfo(metaInfoBuilder.build())
+              .setBindings(bindingsHelper.build())
+              .setPreconditions(preconditions.build())
+              .setConsequences(consequencesBuilder.build())
+              .build());
+    }
+  }
+
   private static class KeepBindingsVisitor extends AnnotationVisitorBase {
     private final ParsingContext parsingContext;
     private final UserBindingsHelper helper;
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/KeepAnnoTestExtractedRulesBase.java b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepAnnoTestExtractedRulesBase.java
index c5aa87a..ee167c1 100644
--- a/src/test/java/com/android/tools/r8/keepanno/androidx/KeepAnnoTestExtractedRulesBase.java
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepAnnoTestExtractedRulesBase.java
@@ -411,6 +411,7 @@
   protected void testExtractedRules(
       Iterable<Class<?>> classes, Iterable<byte[]> classFileData, ExpectedRules expectedRules)
       throws IOException {
+    assumeFalse(parameters.isPG());
     if (parameters.isExtractRules()) {
       List<String> extractedRules = new ArrayList<>();
       for (Class<?> testClass : classes) {
@@ -457,7 +458,8 @@
       Class<?> mainClass,
       List<Class<?>> classes,
       List<byte[]> classFileData,
-      ExpectedRules expectedRules)
+      ExpectedRules expectedRules,
+      String expectedOutput)
       throws Exception {
     // TODO(b/392865072): Proguard 7.4.1 fails with "Encountered corrupt @kotlin/Metadata for class
     // <binary name> (version 2.1.0)", as ti avoid missing classes warnings from ProGuard some of
@@ -477,7 +479,17 @@
               }
             })
         .run(mainClass)
-        .assertSuccessWithOutput(getExpectedOutputForJava());
+        .assertSuccessWithOutput(expectedOutput);
+  }
+
+  protected void testExtractedRulesAndRunJava(
+      Class<?> mainClass,
+      List<Class<?>> classes,
+      List<byte[]> classFileData,
+      ExpectedRules expectedRules)
+      throws Exception {
+    testExtractedRulesAndRunJava(
+        mainClass, classes, classFileData, expectedRules, getExpectedOutputForJava());
   }
 
   protected void testExtractedRulesAndRunJava(List<Class<?>> classes, ExpectedRules expectedRules)
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/KeepUsesReflectionToAccessMethodTest.java b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepUsesReflectionToAccessMethodTest.java
new file mode 100644
index 0000000..4455cb2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepUsesReflectionToAccessMethodTest.java
@@ -0,0 +1,287 @@
+// Copyright (c) 2025, 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.androidx;
+
+import static com.android.tools.r8.ToolHelper.getFilesInTestFolderRelativeToClass;
+
+import androidx.annotation.keep.UsesReflectionToAccessMethod;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.transformers.ClassFileTransformer.AnnotationBuilder;
+import com.android.tools.r8.transformers.ClassFileTransformer.AnnotationContentBuilder;
+import com.android.tools.r8.transformers.ClassFileTransformer.MethodPredicate;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.function.Consumer;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.objectweb.asm.Type;
+
+@RunWith(Parameterized.class)
+public class KeepUsesReflectionToAccessMethodTest extends KeepAnnoTestExtractedRulesBase {
+
+  @Parameterized.Parameters(name = "{0}, {1}")
+  public static Collection<Object[]> data() {
+    // Test with Android 14, which has `java.lang.ClassValue` to avoid having to deal with R8
+    // missing class warnings for tests using the kotlin-reflect library.
+    return buildParameters(
+        createParameters(
+            getTestParameters()
+                .withDexRuntime(Version.V14_0_0)
+                .withDefaultCfRuntime()
+                .withMaximumApiLevel()
+                .build()),
+        getKotlinTestParameters().withLatestCompiler().build());
+  }
+
+  private static Collection<Path> getKotlinSources() {
+    try {
+      return getFilesInTestFolderRelativeToClass(
+          KeepUsesReflectionToAccessMethodTest.class, "kt", "Methods.kt");
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    compilationResults = getCompileMemoizerWithKeepAnnoLib(getKotlinSources());
+  }
+
+  private static ExpectedRules getExpectedRulesJava(
+      Class<?> conditionClass, String... consequentMembers) {
+    Consumer<ExpectedKeepRule.Builder> setCondition =
+        b ->
+            b.setConditionClass(conditionClass)
+                .setConditionMembers("{ void foo(java.lang.Class); }");
+    ExpectedRules.Builder builder = ExpectedRules.builder();
+    for (int i = 0; i < consequentMembers.length; i++) {
+      builder.add(
+          ExpectedKeepRule.builder()
+              .apply(setCondition)
+              .setConsequentClass(KeptClass.class)
+              .setConsequentMembers(consequentMembers[i])
+              .build());
+    }
+    addConsequentKotlinMetadata(builder, b -> b.apply(setCondition));
+    return builder.build();
+  }
+
+  private static ExpectedRules getExpectedRulesKotlin(
+      String conditionClass, String conditionMembers, String... consequentMembers) {
+    Consumer<ExpectedKeepRule.Builder> setCondition =
+        b ->
+            b.setConditionClass("com.android.tools.r8.keepanno.androidx.kt.Methods")
+                .setConditionMembers(conditionMembers);
+    ExpectedRules.Builder builder = ExpectedRules.builder();
+    for (int i = 0; i < consequentMembers.length; i++) {
+      builder.add(
+          ExpectedKeepRule.builder()
+              .apply(setCondition)
+              .setConsequentClass("com.android.tools.r8.keepanno.androidx.kt.MethodsKeptClass")
+              .setConsequentMembers(consequentMembers[i])
+              .build());
+    }
+    addConsequentKotlinMetadata(builder, b -> b.apply(setCondition));
+    return builder.build();
+  }
+
+  private static void buildUsesReflectionToAccessMethod(
+      AnnotationBuilder builder, Object clazz, String methodName, Class<?>... parameterTypes) {
+    AnnotationContentBuilder ab =
+        builder.setAnnotationClass(Reference.classFromClass(UsesReflectionToAccessMethod.class));
+    if (clazz instanceof String) {
+      ab.setField("className", clazz);
+    } else {
+      assert clazz instanceof Class<?> || clazz instanceof Type;
+      ab.setField("classConstant", clazz);
+    }
+    ab.setField("methodName", methodName);
+    // No parameterTypes or parameterTypeNames means any method.
+    if (parameterTypes != null && parameterTypes.length > 0) {
+      ab.setArray("parameterTypes", (Object[]) parameterTypes);
+    }
+    // No returnType or returnTypeName means any return type.
+  }
+
+  private static void buildUsesReflectionToAccessMethodMultiple(
+      AnnotationBuilder builder, Object clazz) {
+    builder
+        .setAnnotationClass(
+            Reference.classFromBinaryName(
+                Reference.classFromClass(UsesReflectionToAccessMethod.class).getBinaryName()
+                    + "$Container"))
+        .buildArray(
+            "value",
+            builder1 ->
+                builder1
+                    .setAnnotationField(
+                        null,
+                        builder2 ->
+                            buildUsesReflectionToAccessMethod(builder2, clazz, "m", int.class))
+                    .setAnnotationField(
+                        null,
+                        builder3 ->
+                            buildUsesReflectionToAccessMethod(
+                                builder3, clazz, "m", int.class, long.class))
+                    .setAnnotationField(
+                        null,
+                        builder4 ->
+                            buildUsesReflectionToAccessMethod(
+                                builder4, clazz, "m", String.class, String.class, String.class)));
+  }
+
+  @Test
+  public void testAnyReturnTypeAndAnyParameters() throws Exception {
+    testExtractedRulesAndRunJava(
+        ClassWithAnnotation.class,
+        ImmutableList.of(KeptClass.class),
+        ImmutableList.of(
+            setAnnotationOnMethod(
+                ClassWithAnnotation.class,
+                MethodPredicate.onName("foo"),
+                builder -> buildUsesReflectionToAccessMethod(builder, KeptClass.class, "m"))),
+        getExpectedRulesJava(ClassWithAnnotation.class, "{ *** m(...); }"),
+        StringUtils.lines("4"));
+  }
+
+  @Test
+  public void testAnyReturnTypeAndIntParameter() throws Exception {
+    testExtractedRulesAndRunJava(
+        ClassWithAnnotation.class,
+        ImmutableList.of(KeptClass.class),
+        ImmutableList.of(
+            setAnnotationOnMethod(
+                ClassWithAnnotation.class,
+                MethodPredicate.onName("foo"),
+                builder ->
+                    buildUsesReflectionToAccessMethod(builder, KeptClass.class, "m", int.class))),
+        getExpectedRulesJava(ClassWithAnnotation.class, "{ *** m(int); }"),
+        parameters.isReference() ? StringUtils.lines("4") : StringUtils.lines("1"));
+  }
+
+  @Test
+  public void testAnyReturnTypeAndMultipleParameterLists() throws Exception {
+    testExtractedRulesAndRunJava(
+        ClassWithAnnotation.class,
+        ImmutableList.of(KeptClass.class),
+        ImmutableList.of(
+            setAnnotationOnMethod(
+                ClassWithAnnotation.class,
+                MethodPredicate.onName("foo"),
+                builder -> buildUsesReflectionToAccessMethodMultiple(builder, KeptClass.class))),
+        getExpectedRulesJava(
+            ClassWithAnnotation.class,
+            "{ *** m(int); }",
+            "{ *** m(int, long); }",
+            "{ *** m(java.lang.String, java.lang.String, java.lang.String); }"),
+        parameters.isReference() ? StringUtils.lines("4") : StringUtils.lines("3"));
+  }
+
+  @Test
+  public void testAnyReturnTypeAndAnyParametersKotlin() throws Exception {
+    testExtractedRulesAndRunKotlin(
+        compilationResults,
+        (classReference, classFileBytes) ->
+            setAnnotationOnMethod(
+                classReference,
+                classFileBytes,
+                Reference.classFromTypeName("com.android.tools.r8.keepanno.androidx.kt.Methods"),
+                MethodPredicate.onName("foo"),
+                builder ->
+                    buildUsesReflectionToAccessMethod(
+                        builder,
+                        Type.getType(
+                            DescriptorUtils.javaTypeToDescriptor(
+                                "com.android.tools.r8.keepanno.androidx.kt.MethodsKeptClass")),
+                        "m")),
+        "com.android.tools.r8.keepanno.androidx.kt.MethodsKt",
+        getExpectedRulesKotlin(
+            "com.android.tools.r8.keepanno.androidx.kt.Methods",
+            "{ void foo(kotlin.reflect.KClass); }",
+            "{ *** m(...); }"),
+        StringUtils.lines("4"));
+  }
+
+  @Test
+  public void testAnyReturnTypeAndIntParameterKotlin() throws Exception {
+    testExtractedRulesAndRunKotlin(
+        compilationResults,
+        (classReference, classFileBytes) ->
+            setAnnotationOnMethod(
+                classReference,
+                classFileBytes,
+                Reference.classFromTypeName("com.android.tools.r8.keepanno.androidx.kt.Methods"),
+                MethodPredicate.onName("foo"),
+                builder ->
+                    buildUsesReflectionToAccessMethod(
+                        builder,
+                        Type.getType(
+                            DescriptorUtils.javaTypeToDescriptor(
+                                "com.android.tools.r8.keepanno.androidx.kt.MethodsKeptClass")),
+                        "m",
+                        int.class)),
+        "com.android.tools.r8.keepanno.androidx.kt.MethodsKt",
+        getExpectedRulesKotlin(
+            "com.android.tools.r8.keepanno.androidx.kt.Methods",
+            "{ void foo(kotlin.reflect.KClass); }",
+            "{ *** m(int); }"),
+        parameters.isReference() ? StringUtils.lines("4") : StringUtils.lines("1"));
+  }
+
+  @Test
+  public void testAnyReturnTypeAndMultipleParameterListsKotlin() throws Exception {
+    testExtractedRules(
+        compilationResults,
+        (classReference, classFileBytes) ->
+            setAnnotationOnMethod(
+                classReference,
+                classFileBytes,
+                Reference.classFromTypeName("com.android.tools.r8.keepanno.androidx.kt.Methods"),
+                MethodPredicate.onName("foo"),
+                builder ->
+                    buildUsesReflectionToAccessMethodMultiple(
+                        builder,
+                        Type.getType(
+                            DescriptorUtils.javaTypeToDescriptor(
+                                "com.android.tools.r8.keepanno.androidx.kt.MethodsKeptClass")))),
+        getExpectedRulesKotlin(
+            "com.android.tools.r8.keepanno.androidx.kt.Methods",
+            "{ void foo(kotlin.reflect.KClass); }",
+            "{ *** m(int); }",
+            "{ *** m(int, long); }",
+            "{ *** m(java.lang.String, java.lang.String, java.lang.String); }"));
+  }
+
+  // Test class without annotation to be used by multiple tests inserting annotations using a
+  // transformer.
+  static class ClassWithAnnotation {
+
+    public void foo(Class<KeptClass> clazz) throws Exception {
+      if (clazz != null) {
+        System.out.println(clazz.getDeclaredMethods().length);
+      }
+    }
+
+    public static void main(String[] args) throws Exception {
+      new ClassWithAnnotation().foo(System.nanoTime() > 0 ? KeptClass.class : null);
+    }
+  }
+
+  static class KeptClass {
+    public void m() {}
+
+    public void m(int i) {}
+
+    public void m(int i, long l) {}
+
+    public void m(String s1, String s2, String s3) {}
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/kt/Methods.kt b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/Methods.kt
new file mode 100644
index 0000000..b61191e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/Methods.kt
@@ -0,0 +1,27 @@
+// Copyright (c) 2025, 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.androidx.kt
+
+import kotlin.reflect.KClass
+import kotlin.reflect.full.declaredMemberFunctions
+
+class Methods {
+  fun foo(clazz: KClass<MethodsKeptClass>?) {
+    println(clazz?.declaredMemberFunctions?.filter { it.name == "m" }?.size)
+  }
+}
+
+class MethodsKeptClass {
+  fun m() {}
+
+  fun m(i: Int) {}
+
+  fun m(i: Int, l: Long) {}
+
+  fun m(s1: String, s2: String, s3: String) {}
+}
+
+fun main() {
+  Methods().foo(if (System.nanoTime() > 0) MethodsKeptClass::class else null)
+}