|  | // 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 static org.hamcrest.CoreMatchers.containsString; | 
|  |  | 
|  | 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 java.util.stream.Collectors; | 
|  | import java.util.stream.Stream; | 
|  | 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 Stream.concat( | 
|  | getFilesInTestFolderRelativeToClass( | 
|  | KeepUsesReflectionToAccessFieldTest.class, "kt", "Fields.kt") | 
|  | .stream(), | 
|  | getFilesInTestFolderRelativeToClass( | 
|  | KeepUsesReflectionToAccessFieldTest.class, "kt", "FieldsPropertyAccess.kt") | 
|  | .stream()) | 
|  | .collect(Collectors.toList()); | 
|  | } catch (IOException e) { | 
|  | throw new RuntimeException(e); | 
|  | } | 
|  | } | 
|  |  | 
|  | @BeforeClass | 
|  | public static void beforeClass() throws Exception { | 
|  | compilationResults = getCompileMemoizerWithKeepAnnoLib(getKotlinSources()); | 
|  | } | 
|  |  | 
|  | private static ExpectedRules getExpectedRulesJava( | 
|  | Class<?> conditionClass, boolean includeSubclasses, 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()); | 
|  | if (includeSubclasses) { | 
|  | builder.add( | 
|  | ExpectedKeepRule.builder() | 
|  | .apply(setCondition) | 
|  | .setConsequentExtendsClass(KeptClass.class) | 
|  | .setConsequentMembers(consequentMembers[i]) | 
|  | .build()); | 
|  | } | 
|  | } | 
|  | addConsequentKotlinMetadata(builder, b -> b.apply(setCondition)); | 
|  | addDefaultInitWorkaround( | 
|  | builder, b -> b.apply(setCondition).setConsequentClass(KeptClass.class)); | 
|  | if (includeSubclasses) { | 
|  | addDefaultInitWorkaround( | 
|  | builder, b -> b.apply(setCondition).setConsequentExtendsClass(KeptClass.class)); | 
|  | } | 
|  | return builder.build(); | 
|  | } | 
|  |  | 
|  | private static ExpectedRules getExpectedRulesJava( | 
|  | Class<?> conditionClass, String... consequentMembers) { | 
|  | return getExpectedRulesJava(conditionClass, false, consequentMembers); | 
|  | } | 
|  |  | 
|  | 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 testIncludeSubclasses() throws Exception { | 
|  | testExtractedRules( | 
|  | ImmutableList.of( | 
|  | setAnnotationOnMethod( | 
|  | ClassWithAnnotation.class, | 
|  | MethodPredicate.onName("foo"), | 
|  | builder -> { | 
|  | builder | 
|  | .setAnnotationClass( | 
|  | Reference.classFromClass(UsesReflectionToAccessField.class)) | 
|  | .setField("classConstant", KeptClass.class) | 
|  | .setField("fieldName", "x") | 
|  | .setField("includeSubclasses", true); | 
|  | })), | 
|  | getExpectedRulesJava(ClassWithAnnotation.class, true, "{ *** x; }")); | 
|  | } | 
|  |  | 
|  | @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 | 
|  | public void testPropertyAccessKotlin() throws Exception { | 
|  | testExtractedRulesAndRunKotlin( | 
|  | compilationResults, | 
|  | (classReference, classFileBytes) -> | 
|  | setAnnotationOnMethod( | 
|  | classReference, | 
|  | classFileBytes, | 
|  | Reference.classFromTypeName( | 
|  | "com.android.tools.r8.keepanno.androidx.kt.FieldsPropertyAccess"), | 
|  | MethodPredicate.onName("foo"), | 
|  | builder -> | 
|  | buildUsesReflectionToAccessField( | 
|  | builder, | 
|  | Type.getType( | 
|  | DescriptorUtils.javaTypeToDescriptor( | 
|  | "com.android.tools.r8.keepanno.androidx.kt.FieldsPropertyAccessKeptClass")), | 
|  | "x", | 
|  | int.class)), | 
|  | "com.android.tools.r8.keepanno.androidx.kt.FieldsPropertyAccessKt", | 
|  | getExpectedRulesKotlin( | 
|  | "com.android.tools.r8.keepanno.androidx.kt.FieldsPropertyAccess", | 
|  | "{ void foo(); }", | 
|  | "com.android.tools.r8.keepanno.androidx.kt.FieldsPropertyAccessKeptClass", | 
|  | "{ int x; }"), | 
|  | // TODO(b/392865072): Not sure why this succeeds on DEX even though the getter getX has | 
|  | //  been removed. | 
|  | parameters.getBackend().isDex() | 
|  | ? r -> r.assertSuccessWithOutput(StringUtils.lines("1")) | 
|  | : r -> | 
|  | r.assertFailureWithErrorThatMatches( | 
|  | containsString( | 
|  | "Property 'x' (JVM signature: getX()I) not resolved in class" | 
|  | + " com.android.tools.r8.keepanno.androidx.kt"))); | 
|  | } | 
|  |  | 
|  | // 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; | 
|  | } | 
|  | } |