Add @UsesReflectionToConstruct test for any constructor

Bug: b/392865072
Bug: b/437277192
Change-Id: Ia961b48602a50576e8362cda5c1fd56b1347ce30
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 2497a5b..1cdc98e 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
@@ -437,7 +437,8 @@
       KotlinCompileMemoizer compilation,
       List<byte[]> classFileData,
       String mainClass,
-      ExpectedRules expectedRules)
+      ExpectedRules expectedRules,
+      String expectedOutput)
       throws Exception {
     // TODO(b/392865072): Legacy R8 fails with AssertionError: Synthetic class kinds should agree.
     assumeFalse(parameters.isLegacyR8());
@@ -491,20 +492,22 @@
               }
             })
         .run(mainClass)
-        .assertSuccessWithOutput(getExpectedOutputForKotlin());
+        .assertSuccessWithOutput(expectedOutput);
   }
 
   protected void runTestExtractedRulesKotlin(
       KotlinCompileMemoizer compilation, String mainClass, ExpectedRules expectedRules)
       throws Exception {
-    runTestExtractedRulesKotlin(compilation, ImmutableList.of(), mainClass, expectedRules);
+    runTestExtractedRulesKotlin(
+        compilation, ImmutableList.of(), mainClass, expectedRules, getExpectedOutputForKotlin());
   }
 
   protected void runTestExtractedRulesKotlin(
       KotlinCompileMemoizer compilation,
       BiFunction<ClassReference, byte[], byte[]> transformerForClass,
       String mainClass,
-      ExpectedRules expectedRules)
+      ExpectedRules expectedRules,
+      String expectedOutput)
       throws Exception {
     List<byte[]> result = new ArrayList<>();
     ZipUtils.iter(
@@ -517,6 +520,16 @@
           result.add(
               transformerForClass.apply(classReference, ByteStreams.toByteArray(inputStream)));
         });
-    runTestExtractedRulesKotlin(null, result, mainClass, expectedRules);
+    runTestExtractedRulesKotlin(null, result, mainClass, expectedRules, expectedOutput);
+  }
+
+  protected void runTestExtractedRulesKotlin(
+      KotlinCompileMemoizer compilation,
+      BiFunction<ClassReference, byte[], byte[]> transformerForClass,
+      String mainClass,
+      ExpectedRules expectedRules)
+      throws Exception {
+    runTestExtractedRulesKotlin(
+        compilation, transformerForClass, mainClass, expectedRules, getExpectedOutputForKotlin());
   }
 }
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/KeepUsesReflectionForInstantiationAnyArgsConstructorTest.java b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepUsesReflectionForInstantiationAnyArgsConstructorTest.java
new file mode 100644
index 0000000..dfb602f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepUsesReflectionForInstantiationAnyArgsConstructorTest.java
@@ -0,0 +1,224 @@
+// 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.UsesReflectionToConstruct;
+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.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 KeepUsesReflectionForInstantiationAnyArgsConstructorTest
+    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());
+  }
+
+  @Override
+  protected String getExpectedOutputForJava() {
+    return StringUtils.lines("4");
+  }
+
+  @Override
+  protected String getExpectedOutputForKotlin() {
+    return StringUtils.lines(
+        "fun `<init>`(): com.android.tools.r8.keepanno.androidx.kt.KeptClass", "<init>()", "4");
+  }
+
+  private static Collection<Path> getKotlinSources() {
+    try {
+      return getFilesInTestFolderRelativeToClass(
+          KeepUsesReflectionForInstantiationAnyArgsConstructorTest.class,
+          "kt",
+          "AnyArgsConstructor.kt",
+          "KeptClass.kt");
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    compilationResults = getCompileMemoizerWithKeepAnnoLib(getKotlinSources());
+  }
+
+  private static ExpectedRules getExpectedRulesJava(Class<?> conditionClass) {
+    return getExpectedRulesJava(conditionClass, null);
+  }
+
+  private static ExpectedRules getExpectedRulesJava(
+      Class<?> conditionClass, String conditionMembers) {
+    Consumer<ExpectedKeepRule.Builder> setCondition =
+        b -> b.setConditionClass(conditionClass).setConditionMembers(conditionMembers);
+    ExpectedRules.Builder builder =
+        ExpectedRules.builder()
+            .add(
+                ExpectedKeepRule.builder()
+                    .apply(setCondition)
+                    .setConsequentClass(KeptClass.class)
+                    .setConsequentMembers("{ void <init>(...); }")
+                    .build());
+    addConsequentKotlinMetadata(builder, b -> b.apply(setCondition));
+    return builder.build();
+  }
+
+  private static ExpectedRules getExpectedRulesKotlin(String conditionClass) {
+    return getExpectedRulesKotlin(conditionClass, null);
+  }
+
+  private static ExpectedRules getExpectedRulesKotlin(
+      String conditionClass, String conditionMembers) {
+    Consumer<ExpectedKeepRule.Builder> setCondition =
+        b ->
+            b.setConditionClass("com.android.tools.r8.keepanno.androidx.kt.AnyArgsConstructor")
+                .setConditionMembers(conditionMembers);
+    ExpectedRules.Builder builder =
+        ExpectedRules.builder()
+            .add(
+                ExpectedKeepRule.builder()
+                    .apply(setCondition)
+                    .setConsequentClass("com.android.tools.r8.keepanno.androidx.kt.KeptClass")
+                    .setConsequentMembers("{ void <init>(...); }")
+                    .build());
+    addConsequentKotlinMetadata(builder, b -> b.apply(setCondition));
+    return builder.build();
+  }
+
+  private static void buildAnyConstructor(AnnotationBuilder builder, Object clazz) {
+    if (clazz instanceof String) {
+      builder.setField("className", clazz);
+    } else {
+      assert clazz instanceof Class<?> || clazz instanceof Type;
+      builder.setField("classConstant", clazz);
+    }
+    // No parameterTypes or parameterTypeNames means any constructor.
+  }
+
+  @Test
+  public void testAnyConstructor() throws Exception {
+    runTestExtractedRulesJava(
+        AnyConstructor.class,
+        ImmutableList.of(KeptClass.class),
+        ImmutableList.of(
+            setAnnotationOnMethod(
+                AnyConstructor.class,
+                MethodPredicate.onName("foo"),
+                UsesReflectionToConstruct.class,
+                builder -> buildAnyConstructor(builder, KeptClass.class))),
+        getExpectedRulesJava(AnyConstructor.class, "{ void foo(java.lang.Class); }"));
+  }
+
+  @Test
+  public void testAnyConstructorAnnotateClass() throws Exception {
+    runTestExtractedRulesJava(
+        AnyConstructor.class,
+        ImmutableList.of(KeptClass.class),
+        ImmutableList.of(
+            setAnnotationOnClass(
+                AnyConstructor.class,
+                UsesReflectionToConstruct.class,
+                builder -> buildAnyConstructor(builder, KeptClass.class))),
+        getExpectedRulesJava(AnyConstructor.class));
+  }
+
+  @Test
+  public void testAnyConstructorKotlin() throws Exception {
+    runTestExtractedRulesKotlin(
+        compilationResults,
+        (classReference, classFileBytes) ->
+            setAnnotationOnMethod(
+                classReference,
+                classFileBytes,
+                Reference.classFromTypeName(
+                    "com.android.tools.r8.keepanno.androidx.kt.AnyArgsConstructor"),
+                MethodPredicate.onName("foo"),
+                UsesReflectionToConstruct.class,
+                builder ->
+                    buildAnyConstructor(
+                        builder,
+                        Type.getType(
+                            DescriptorUtils.javaTypeToDescriptor(
+                                "com.android.tools.r8.keepanno.androidx.kt.KeptClass")))),
+        "com.android.tools.r8.keepanno.androidx.kt.AnyArgsConstructorKt",
+        getExpectedRulesKotlin(
+            "com.android.tools.r8.keepanno.androidx.kt.AnyArgsConstructor",
+            "{ void foo(kotlin.reflect.KClass); }"));
+  }
+
+  @Test
+  public void testAnyConstructorKotlinAnnotateClass() throws Exception {
+    runTestExtractedRulesKotlin(
+        compilationResults,
+        (classReference, classFileBytes) ->
+            setAnnotationOnClass(
+                classReference,
+                classFileBytes,
+                Reference.classFromTypeName(
+                    "com.android.tools.r8.keepanno.androidx.kt.AnyArgsConstructor"),
+                UsesReflectionToConstruct.class,
+                builder ->
+                    buildAnyConstructor(
+                        builder,
+                        Type.getType(
+                            DescriptorUtils.javaTypeToDescriptor(
+                                "com.android.tools.r8.keepanno.androidx.kt.KeptClass")))),
+        "com.android.tools.r8.keepanno.androidx.kt.AnyArgsConstructorKt",
+        getExpectedRulesKotlin("com.android.tools.r8.keepanno.androidx.kt.AnyArgsConstructor"),
+        // TODO(b/437277192): Constructors should be kept.
+        parameters.isExtractRules()
+            ? StringUtils.lines("null", "0")
+            : getExpectedOutputForKotlin());
+  }
+
+  // Test class without annotation to be used by multiple tests inserting annotations using a
+  // transformer.
+  static class AnyConstructor {
+
+    public void foo(Class<KeptClass> clazz) throws Exception {
+      if (clazz != null) {
+        System.out.println(clazz.getDeclaredConstructors().length);
+      }
+    }
+
+    public static void main(String[] args) throws Exception {
+      new AnyConstructor().foo(System.nanoTime() > 0 ? KeptClass.class : null);
+    }
+  }
+
+  static class KeptClass {
+    KeptClass() {}
+
+    KeptClass(int i) {}
+
+    KeptClass(long j) {}
+
+    KeptClass(String s1, String s2, String s3) {}
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/kt/AnyArgsConstructor.kt b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/AnyArgsConstructor.kt
new file mode 100644
index 0000000..c891829
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/AnyArgsConstructor.kt
@@ -0,0 +1,19 @@
+// 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.primaryConstructor
+
+class AnyArgsConstructor {
+  fun foo(clazz: KClass<KeptClass>?) {
+    println(clazz?.primaryConstructor)
+    clazz?.primaryConstructor?.call()
+    println(clazz?.constructors?.size)
+  }
+}
+
+fun main() {
+  AnyArgsConstructor().foo(if (System.nanoTime() > 0) KeptClass::class else null)
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/kt/KeptClass.kt b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/KeptClass.kt
index 89e2d02..e96643d 100644
--- a/src/test/java/com/android/tools/r8/keepanno/androidx/kt/KeptClass.kt
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/KeptClass.kt
@@ -16,7 +16,7 @@
     println("<init>(Long)")
   }
 
-  constructor(s: String) : this() {
-    println("<init>(String)")
+  constructor(s1: String, s2: String, s3: String) : this() {
+    println("<init>(String, String, String)")
   }
 }