[keepanno] Initial implementation of support for @UsesReflectiontoAccessField
Bug: b/392865072
Change-Id: I4e6341d60e7d1d6b17746bab321d30cce7e89095
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 e409d74..8f7ad00 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.UsesReflectionToAccessField;
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;
@@ -109,6 +110,7 @@
|| AnnotationConstants.UsesReflection.isDescriptor(descriptor)
|| AnnotationConstants.UsesReflectionToConstruct.isDescriptor(descriptor)
|| AnnotationConstants.UsesReflectionToAccessMethod.isDescriptor(descriptor)
+ || AnnotationConstants.UsesReflectionToAccessField.isDescriptor(descriptor)
|| AnnotationConstants.ForApi.isDescriptor(descriptor)
|| AnnotationConstants.UsedByReflection.isDescriptor(descriptor)
|| AnnotationConstants.UsedByNative.isDescriptor(descriptor)
@@ -117,6 +119,8 @@
|| AnnotationConstants.UsesReflectionToConstruct.isKotlinRepeatableContainerDescriptor(
descriptor)
|| AnnotationConstants.UsesReflectionToAccessMethod.isKotlinRepeatableContainerDescriptor(
+ descriptor)
+ || AnnotationConstants.UsesReflectionToAccessField.isKotlinRepeatableContainerDescriptor(
descriptor)) {
return true;
}
@@ -391,6 +395,16 @@
.setClassNamePattern(KeepQualifiedClassNamePattern.exact(className))
.build());
}
+ if (AnnotationConstants.UsesReflectionToAccessField.isDescriptor(descriptor)) {
+ return new UsesReflectionToAccessFieldVisitor(
+ parsingContext,
+ parent::accept,
+ setContext,
+ bindingsHelper ->
+ KeepClassItemPattern.builder()
+ .setClassNamePattern(KeepQualifiedClassNamePattern.exact(className))
+ .build());
+ }
if (ForApi.isDescriptor(descriptor)) {
return new ForApiClassVisitor(parsingContext, parent::accept, setContext, className);
}
@@ -567,6 +581,23 @@
bindingsHelper ->
createMethodItemContext(className, methodName, methodDescriptor, bindingsHelper));
}
+ if (AnnotationConstants.UsesReflectionToAccessField.isDescriptor(descriptor)) {
+ return new UsesReflectionToAccessFieldVisitor(
+ parsingContext,
+ parent::accept,
+ setContext,
+ bindingsHelper ->
+ createMethodItemContext(className, methodName, methodDescriptor, bindingsHelper));
+ }
+ if (AnnotationConstants.UsesReflectionToAccessField.isKotlinRepeatableContainerDescriptor(
+ descriptor)) {
+ return new UsesReflectionToAccessFieldContainerVisitor(
+ parsingContext,
+ parent::accept,
+ setContext,
+ bindingsHelper ->
+ createMethodItemContext(className, methodName, methodDescriptor, bindingsHelper));
+ }
if (AnnotationConstants.ForApi.isDescriptor(descriptor)) {
return new ForApiMemberVisitor(
parsingContext,
@@ -1910,6 +1941,209 @@
}
}
+ private static class UsesReflectionToAccessFieldContainerVisitor extends AnnotationVisitorBase {
+
+ private final AnnotationParsingContext parsingContext;
+ private final Parent<KeepEdge> parent;
+ private final Consumer<KeepEdgeMetaInfo.Builder> addContext;
+ private final Function<UserBindingsHelper, KeepItemPattern> contextBuilder;
+
+ UsesReflectionToAccessFieldContainerVisitor(
+ 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 UsesReflectionToAccessFieldContainerElementVisitor(
+ 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 UsesReflectionToAccessFieldContainerElementVisitor
+ extends AnnotationVisitorBase {
+ private final AnnotationParsingContext parsingContext;
+ private final Parent<KeepEdge> parent;
+ Consumer<KeepEdgeMetaInfo.Builder> addContext;
+ Function<UserBindingsHelper, KeepItemPattern> contextBuilder;
+
+ public UsesReflectionToAccessFieldContainerElementVisitor(
+ 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.UsesReflectionToAccessField.isDescriptor(descriptor)) {
+ return new UsesReflectionToAccessFieldVisitor(
+ parsingContext, parent, addContext, contextBuilder);
+ }
+ return super.visitAnnotation(name, descriptor);
+ }
+
+ @Override
+ public void visit(String name, Object value) {
+ super.visit(name, value);
+ }
+ }
+
+ private static class UsesReflectionToAccessFieldVisitor 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 KeepFieldNamePattern fieldName;
+ private KeepFieldTypePattern fieldType = KeepFieldTypePattern.any();
+
+ UsesReflectionToAccessFieldVisitor(
+ 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(UsesReflectionToAccessField.classConstant) && value instanceof Type) {
+ qualifiedName =
+ KeepQualifiedClassNamePattern.exactFromDescriptor(((Type) value).getDescriptor());
+ return;
+ }
+ if (name.equals(UsesReflectionToAccessField.className) && value instanceof String) {
+ qualifiedName = KeepQualifiedClassNamePattern.exact((String) value);
+ return;
+ }
+ if (name.equals(UsesReflectionToAccessField.fieldName) && value instanceof String) {
+ fieldName = KeepFieldNamePattern.exact((String) value);
+ return;
+ }
+ if (name.equals(UsesReflectionToAccessField.fieldType) && value instanceof Type) {
+ fieldType =
+ KeepFieldTypePattern.fromType(
+ KeepTypePattern.fromDescriptor(((Type) value).getDescriptor()));
+ return;
+ }
+ if (name.equals(UsesReflectionToAccessField.fieldTypeName) && value instanceof String) {
+ fieldType =
+ KeepFieldTypePattern.fromType(
+ KeepTypePattern.fromClass(
+ KeepClassPattern.fromName(
+ KeepQualifiedClassNamePattern.exact((String) value))));
+ return;
+ }
+ super.visit(name, value);
+ }
+
+ @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(
+ KeepFieldPattern.builder()
+ .setNamePattern(fieldName)
+ .setTypePattern(fieldType)
+ .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).build())
+ .addTarget(KeepTarget.builder().setItemReference(memberBinding).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/KeepUsesReflectionToAccessFieldTest.java b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepUsesReflectionToAccessFieldTest.java
new file mode 100644
index 0000000..d5ce9e4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepUsesReflectionToAccessFieldTest.java
@@ -0,0 +1,292 @@
+// 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.UsesReflectionToAccessField;
+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 KeepUsesReflectionToAccessFieldTest 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(
+ KeepUsesReflectionToAccessFieldTest.class, "kt", "Fields.kt", "FieldsKeptClass.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) {
+ java.util.function.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));
+ addDefaultInitWorkaround(
+ builder, b -> b.apply(setCondition).setConsequentClass(KeptClass.class));
+ return builder.build();
+ }
+
+ private static ExpectedRules getExpectedRulesKotlin(
+ String conditionClass,
+ String conditionMembers,
+ String consequentClass,
+ String... consequentMembers) {
+ Consumer<ExpectedKeepRule.Builder> setCondition =
+ b -> b.setConditionClass(conditionClass).setConditionMembers(conditionMembers);
+ ExpectedRules.Builder builder = ExpectedRules.builder();
+ for (int i = 0; i < consequentMembers.length; i++) {
+ builder.add(
+ ExpectedKeepRule.builder()
+ .apply(setCondition)
+ .setConsequentClass(consequentClass)
+ .setConsequentMembers(consequentMembers[i])
+ .build());
+ }
+ addConsequentKotlinMetadata(builder, b -> b.apply(setCondition));
+ addDefaultInitWorkaround(
+ builder, b -> b.apply(setCondition).setConsequentClass(consequentClass));
+ return builder.build();
+ }
+
+ private static void buildUsesReflectionToAccessField(
+ AnnotationBuilder builder, Object clazz, String fieldName, Class<?> fieldType) {
+ AnnotationContentBuilder ab =
+ builder.setAnnotationClass(Reference.classFromClass(UsesReflectionToAccessField.class));
+ if (clazz instanceof String) {
+ ab.setField("className", clazz);
+ } else {
+ assert clazz instanceof Class<?> || clazz instanceof Type;
+ ab.setField("classConstant", clazz);
+ }
+ ab.setField("fieldName", fieldName);
+ // No fieldType means any field type.
+ if (fieldType != null) {
+ ab.setField("fieldType", fieldType);
+ }
+ }
+
+ private static void buildUsesReflectionToAccessField(
+ AnnotationBuilder builder, Object clazz, String fieldName) {
+ buildUsesReflectionToAccessField(builder, clazz, fieldName, null);
+ }
+
+ private static void buildUsesReflectionToAccessFieldMultiple(
+ AnnotationBuilder builder, Object clazz) {
+ builder
+ .setAnnotationClass(
+ Reference.classFromBinaryName(
+ Reference.classFromClass(UsesReflectionToAccessField.class).getBinaryName()
+ + "$Container"))
+ .buildArray(
+ "value",
+ builder1 ->
+ builder1
+ .setAnnotationField(
+ null,
+ builder2 ->
+ buildUsesReflectionToAccessField(builder2, clazz, "x", int.class))
+ .setAnnotationField(
+ null,
+ builder3 ->
+ buildUsesReflectionToAccessField(builder3, clazz, "y", long.class))
+ .setAnnotationField(
+ null,
+ builder4 ->
+ buildUsesReflectionToAccessField(builder4, clazz, "s", String.class)));
+ }
+
+ @Test
+ public void testAnyFieldType() throws Exception {
+ testExtractedRulesAndRunJava(
+ ClassWithAnnotation.class,
+ ImmutableList.of(KeptClass.class),
+ ImmutableList.of(
+ setAnnotationOnMethod(
+ ClassWithAnnotation.class,
+ MethodPredicate.onName("foo"),
+ builder -> buildUsesReflectionToAccessField(builder, KeptClass.class, "x"))),
+ getExpectedRulesJava(ClassWithAnnotation.class, "{ *** x; }"),
+ parameters.isReference() ? StringUtils.lines("3") : StringUtils.lines("1"));
+ }
+
+ @Test
+ public void testIntFieldType() throws Exception {
+ testExtractedRulesAndRunJava(
+ ClassWithAnnotation.class,
+ ImmutableList.of(KeptClass.class),
+ ImmutableList.of(
+ setAnnotationOnMethod(
+ ClassWithAnnotation.class,
+ MethodPredicate.onName("foo"),
+ builder ->
+ buildUsesReflectionToAccessField(builder, KeptClass.class, "x", int.class))),
+ getExpectedRulesJava(ClassWithAnnotation.class, "{ int x; }"),
+ parameters.isReference() ? StringUtils.lines("3") : StringUtils.lines("1"));
+ }
+
+ @Test
+ public void testMultipleFieldTypes() throws Exception {
+ testExtractedRulesAndRunJava(
+ ClassWithAnnotation.class,
+ ImmutableList.of(KeptClass.class),
+ ImmutableList.of(
+ setAnnotationOnMethod(
+ ClassWithAnnotation.class,
+ MethodPredicate.onName("foo"),
+ builder -> buildUsesReflectionToAccessFieldMultiple(builder, KeptClass.class))),
+ getExpectedRulesJava(
+ ClassWithAnnotation.class, "{ int x; }", "{ long y; }", "{ java.lang.String s; }"),
+ StringUtils.lines("3"));
+ }
+
+ @Test
+ public void testAnyFieldTypeKotlin() throws Exception {
+ testExtractedRulesAndRunKotlin(
+ compilationResults,
+ (classReference, classFileBytes) ->
+ setAnnotationOnMethod(
+ classReference,
+ classFileBytes,
+ Reference.classFromTypeName("com.android.tools.r8.keepanno.androidx.kt.Fields"),
+ MethodPredicate.onName("foo"),
+ builder ->
+ buildUsesReflectionToAccessField(
+ builder,
+ Type.getType(
+ DescriptorUtils.javaTypeToDescriptor(
+ "com.android.tools.r8.keepanno.androidx.kt.FieldsKeptClass")),
+ "x")),
+ "com.android.tools.r8.keepanno.androidx.kt.FieldsKt",
+ getExpectedRulesKotlin(
+ "com.android.tools.r8.keepanno.androidx.kt.Fields",
+ "{ void foo(kotlin.reflect.KClass); }",
+ "com.android.tools.r8.keepanno.androidx.kt.FieldsKeptClass",
+ "{ *** x; }"),
+ StringUtils.lines("1"));
+ }
+
+ @Test
+ public void testIntFieldTypeKotlin() throws Exception {
+ testExtractedRulesAndRunKotlin(
+ compilationResults,
+ (classReference, classFileBytes) ->
+ setAnnotationOnMethod(
+ classReference,
+ classFileBytes,
+ Reference.classFromTypeName("com.android.tools.r8.keepanno.androidx.kt.Fields"),
+ MethodPredicate.onName("foo"),
+ builder ->
+ buildUsesReflectionToAccessField(
+ builder,
+ Type.getType(
+ DescriptorUtils.javaTypeToDescriptor(
+ "com.android.tools.r8.keepanno.androidx.kt.FieldsKeptClass")),
+ "x",
+ int.class)),
+ "com.android.tools.r8.keepanno.androidx.kt.FieldsKt",
+ getExpectedRulesKotlin(
+ "com.android.tools.r8.keepanno.androidx.kt.Fields",
+ "{ void foo(kotlin.reflect.KClass); }",
+ "com.android.tools.r8.keepanno.androidx.kt.FieldsKeptClass",
+ "{ int x; }"),
+ StringUtils.lines("1"));
+ }
+
+ @Test
+ public void testMultipleFieldsKotlin() throws Exception {
+ testExtractedRulesAndRunKotlin(
+ compilationResults,
+ (classReference, classFileBytes) ->
+ setAnnotationOnMethod(
+ classReference,
+ classFileBytes,
+ Reference.classFromTypeName("com.android.tools.r8.keepanno.androidx.kt.Fields"),
+ MethodPredicate.onName("foo"),
+ builder ->
+ buildUsesReflectionToAccessFieldMultiple(
+ builder,
+ Type.getType(
+ DescriptorUtils.javaTypeToDescriptor(
+ "com.android.tools.r8.keepanno.androidx.kt.FieldsKeptClass")))),
+ "com.android.tools.r8.keepanno.androidx.kt.FieldsKt",
+ getExpectedRulesKotlin(
+ "com.android.tools.r8.keepanno.androidx.kt.Fields",
+ "{ void foo(kotlin.reflect.KClass); }",
+ "com.android.tools.r8.keepanno.androidx.kt.FieldsKeptClass",
+ "{ int x; }",
+ "{ long y; }",
+ "{ java.lang.String s; }"),
+ StringUtils.lines("3"));
+ }
+
+ // 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.getDeclaredFields().length);
+ }
+ }
+
+ public static void main(String[] args) throws Exception {
+ new ClassWithAnnotation().foo(System.nanoTime() > 0 ? KeptClass.class : null);
+ }
+ }
+
+ static class KeptClass {
+ public int x;
+ public long y;
+ public String s;
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/kt/Fields.kt b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/Fields.kt
new file mode 100644
index 0000000..13dabea
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/Fields.kt
@@ -0,0 +1,25 @@
+// 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.declaredMemberProperties
+
+class Fields {
+ fun foo(clazz: KClass<FieldsKeptClass>?) {
+ println(clazz?.declaredMemberProperties?.size)
+ }
+}
+
+class FieldsKeptClass {
+ var x: Int = 0
+
+ var y: Long = 0
+
+ var s: String = ""
+}
+
+fun main() {
+ Fields().foo(if (System.nanoTime() > 0) FieldsKeptClass::class else null)
+}