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)")
}
}