[TraceReferences] Add tracing of annotation values
Bug: b/376016627
Change-Id: I3ae4992680a74a1a73485247737f00b0fb2c682a
diff --git a/src/main/java/com/android/tools/r8/tracereferences/KeepRuleFormatter.java b/src/main/java/com/android/tools/r8/tracereferences/KeepRuleFormatter.java
index b768c52..e36b7b1 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/KeepRuleFormatter.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/KeepRuleFormatter.java
@@ -25,7 +25,12 @@
}
append(allowObfuscation ? "-keep,allowobfuscation" : "-keep");
if (tracedClass.getAccessFlags().isInterface()) {
- appendLine(" interface " + tracedClass.getReference().getTypeName() + " {");
+ appendLine(
+ " "
+ + (tracedClass.getAccessFlags().isAnnotation() ? "@" : "")
+ + "interface "
+ + tracedClass.getReference().getTypeName()
+ + " {");
} else if (tracedClass.getAccessFlags().isEnum()) {
appendLine(" enum " + tracedClass.getReference().getTypeName() + " {");
} else {
diff --git a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesConsumer.java b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesConsumer.java
index 2f6e23d..b40e0a3 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesConsumer.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesConsumer.java
@@ -38,6 +38,8 @@
boolean isInterface();
boolean isEnum();
+
+ boolean isAnnotation();
}
@KeepForApi
diff --git a/src/main/java/com/android/tools/r8/tracereferences/Tracer.java b/src/main/java/com/android/tools/r8/tracereferences/Tracer.java
index 53af34e..8c300e3 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/Tracer.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/Tracer.java
@@ -15,6 +15,7 @@
import com.android.tools.r8.graph.DexClassAndField;
import com.android.tools.r8.graph.DexClassAndMember;
import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexEncodedAnnotation;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexItemFactory;
@@ -48,6 +49,7 @@
import com.android.tools.r8.utils.BooleanBox;
import java.util.HashSet;
import java.util.Set;
+import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
@@ -148,12 +150,15 @@
}
}
- private void addClassType(DexType type, DefinitionContext referencedFrom) {
+ private void addClassType(
+ DexType type,
+ DefinitionContext referencedFrom,
+ Consumer<DexClass> resolvedClassesConsumer) {
assert type.isClassType();
ClassResolutionResult result =
appView.contextIndependentDefinitionForWithResolutionResult(type);
if (result.hasClassResolutionResult()) {
- result.forEachClassResolutionResult(clazz -> addClass(clazz, referencedFrom));
+ result.forEachClassResolutionResult(resolvedClassesConsumer);
} else {
TracedClassImpl tracedClass = new TracedClassImpl(type, referencedFrom);
collectMissingClass(tracedClass);
@@ -161,6 +166,10 @@
}
}
+ private void addClassType(DexType type, DefinitionContext referencedFrom) {
+ addClassType(type, referencedFrom, clazz -> addClass(clazz, referencedFrom));
+ }
+
private void addClass(DexClass clazz, DefinitionContext referencedFrom) {
if (isTargetType(clazz.getType())) {
TracedClassImpl tracedClass = new TracedClassImpl(clazz, referencedFrom);
@@ -281,7 +290,6 @@
DexType type = annotation.getAnnotationType();
assert type.isClassType();
if (type.isIdenticalTo(factory.annotationThrows)
- || type.isIdenticalTo(factory.annotationDefault)
|| type.isIdenticalTo(factory.annotationMethodParameters)
|| type.isIdenticalTo(factory.annotationReachabilitySensitive)
|| type.getDescriptor().startsWith(factory.dalvikAnnotationOptimizationPrefix)
@@ -301,12 +309,55 @@
// (*) Not officially supported and documented.
return;
}
+ if (type.isIdenticalTo(factory.annotationDefault)) {
+ annotation
+ .getAnnotation()
+ .forEachElement(
+ element -> {
+ assert element.getValue().isDexValueAnnotation();
+ registerEncodedAnnotation(
+ element.getValue().asDexValueAnnotation().getValue(), referencedFrom);
+ });
+ return;
+ }
assert !type.getDescriptor().startsWith(factory.dalvikAnnotationPrefix)
: "Unexpected annotation with prefix "
+ factory.dalvikAnnotationPrefix
+ ": "
+ type.getDescriptor();
- addClassType(type, referencedFrom);
+ registerEncodedAnnotation(annotation.getAnnotation(), referencedFrom);
+ }
+
+ void registerEncodedAnnotation(
+ DexEncodedAnnotation annotation, DefinitionContext referencedFrom) {
+ addClassType(
+ annotation.getType(),
+ referencedFrom,
+ resolvedClass -> {
+ addClass(resolvedClass, referencedFrom);
+ // For annotations in target handle annotation "methods" used to set values.
+ annotation.forEachElement(
+ element -> {
+ for (DexEncodedMethod method : resolvedClass.methods()) {
+ if (method.getName().isIdenticalTo(element.name)) {
+ TracedMethodImpl tracedMethod = new TracedMethodImpl(method, referencedFrom);
+ consumer.acceptMethod(tracedMethod, diagnostics);
+ }
+ }
+ // Handle the argument values passed to the annotation "method".
+ registerDexValue(element.getValue(), referencedFrom);
+ });
+ });
+ }
+
+ private void registerDexValue(DexValue value, DefinitionContext referencedFrom) {
+ if (value.isDexValueType()) {
+ addType(value.asDexValueType().getValue(), referencedFrom);
+ } else if (value.isDexValueArray()) {
+ for (DexValue elementValue : value.asDexValueArray().getValues()) {
+ registerDexValue(elementValue, referencedFrom);
+ }
+ }
}
class MethodUseCollector extends UseRegistry<ProgramMethod> {
diff --git a/src/main/java/com/android/tools/r8/tracereferences/internal/ClassAccessFlagsImpl.java b/src/main/java/com/android/tools/r8/tracereferences/internal/ClassAccessFlagsImpl.java
index a5753d2..1580fc5 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/internal/ClassAccessFlagsImpl.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/internal/ClassAccessFlagsImpl.java
@@ -22,4 +22,9 @@
public boolean isEnum() {
return accessFlags.isEnum();
}
+
+ @Override
+ public boolean isAnnotation() {
+ return accessFlags.isAnnotation();
+ }
}
diff --git a/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesAnnotationReferencesInDexTest.java b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesAnnotationReferencesInDexTest.java
new file mode 100644
index 0000000..4ecfb4c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesAnnotationReferencesInDexTest.java
@@ -0,0 +1,164 @@
+// Copyright (c) 2024, 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.tracereferences;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.fail;
+
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableSet;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class TraceReferencesAnnotationReferencesInDexTest extends TestBase {
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withNoneRuntime().build();
+ }
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ static class Consumer implements TraceReferencesConsumer {
+
+ Set<ClassReference> tracedTypes = new HashSet<>();
+ Set<MethodReference> tracedMethods = new HashSet<>();
+
+ @Override
+ public void acceptType(TracedClass tracedClass, DiagnosticsHandler handler) {
+ assertFalse(tracedClass.isMissingDefinition());
+ tracedTypes.add(tracedClass.getReference());
+ }
+
+ @Override
+ public void acceptField(TracedField tracedField, DiagnosticsHandler handler) {
+ fail();
+ }
+
+ @Override
+ public void acceptMethod(TracedMethod tracedMethod, DiagnosticsHandler handler) {
+ fail();
+ }
+ }
+
+ private void runTest(Path sourceDex, TraceReferencesConsumer consumer) throws Exception {
+ testForTraceReferences()
+ .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.LATEST))
+ .addSourceFiles(sourceDex)
+ .addTargetClasses(
+ ClassAnnotation.class,
+ FieldAnnotation.class,
+ MethodAnnotation.class,
+ ConstructorAnnotation.class,
+ ParameterAnnotation.class)
+ .setConsumer(consumer)
+ .trace();
+ }
+
+ private void test(Path sourceDex) throws Exception {
+ Consumer consumer = new Consumer();
+ runTest(sourceDex, consumer);
+ assertEquals(
+ ImmutableSet.of(
+ Reference.classFromClass(ClassAnnotation.class),
+ Reference.classFromClass(ConstructorAnnotation.class),
+ Reference.classFromClass(FieldAnnotation.class),
+ Reference.classFromClass(MethodAnnotation.class),
+ Reference.classFromClass(ParameterAnnotation.class)),
+ consumer.tracedTypes);
+ }
+
+ private void testGeneratedKeepRules(Path sourceDex) throws Exception {
+ StringBuilder keepRulesBuilder = new StringBuilder();
+ runTest(
+ sourceDex,
+ TraceReferencesKeepRules.builder()
+ .setOutputConsumer((string, handler) -> keepRulesBuilder.append(string))
+ .build());
+ String expected =
+ StringUtils.lines(
+ "-keep @interface " + ClassAnnotation.class.getTypeName() + " {",
+ "}",
+ "-keep @interface " + ConstructorAnnotation.class.getTypeName() + " {",
+ "}",
+ "-keep @interface " + FieldAnnotation.class.getTypeName() + " {",
+ "}",
+ "-keep @interface " + MethodAnnotation.class.getTypeName() + " {",
+ "}",
+ "-keep @interface " + ParameterAnnotation.class.getTypeName() + " {",
+ "}");
+ assertEquals(expected, keepRulesBuilder.toString());
+ }
+
+ @Test
+ public void testDexArchive() throws Throwable {
+ Path archive = testForD8(Backend.DEX).addProgramClasses(Source.class).compile().writeToZip();
+ test(archive);
+ testGeneratedKeepRules(archive);
+ }
+
+ @Test
+ public void testDexFile() throws Throwable {
+ Path dex =
+ testForD8(Backend.DEX)
+ .addProgramClasses(Source.class)
+ .compile()
+ .writeToDirectory()
+ .resolve("classes.dex");
+ test(dex);
+ testGeneratedKeepRules(dex);
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.TYPE)
+ public @interface ClassAnnotation {}
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.FIELD)
+ public @interface FieldAnnotation {}
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.METHOD)
+ public @interface MethodAnnotation {}
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.CONSTRUCTOR)
+ public @interface ConstructorAnnotation {}
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.PARAMETER)
+ public @interface ParameterAnnotation {}
+
+ @ClassAnnotation
+ static class Source {
+ @FieldAnnotation public static int field;
+
+ @ConstructorAnnotation
+ public Source() {}
+
+ @MethodAnnotation
+ public static void source(@ParameterAnnotation int param) {}
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesAnnotationValuesReferencesInDexTest.java b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesAnnotationValuesReferencesInDexTest.java
new file mode 100644
index 0000000..b19958b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesAnnotationValuesReferencesInDexTest.java
@@ -0,0 +1,215 @@
+// Copyright (c) 2024, 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.tracereferences;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.fail;
+
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class TraceReferencesAnnotationValuesReferencesInDexTest extends TestBase {
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withNoneRuntime().build();
+ }
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ private static List<Class<?>> SOURCE_CLASSES =
+ ImmutableList.of(
+ Source.class,
+ SourceAnnotationWithClassConstant.class,
+ SourceAnnotationWithClassConstantArray.class);
+
+ static class Consumer implements TraceReferencesConsumer {
+
+ Set<ClassReference> tracedTypes = new HashSet<>();
+ Set<MethodReference> tracedMethods = new HashSet<>();
+
+ @Override
+ public void acceptType(TracedClass tracedClass, DiagnosticsHandler handler) {
+ assertFalse(tracedClass.isMissingDefinition());
+ tracedTypes.add(tracedClass.getReference());
+ }
+
+ @Override
+ public void acceptField(TracedField tracedField, DiagnosticsHandler handler) {
+ fail();
+ }
+
+ @Override
+ public void acceptMethod(TracedMethod tracedMethod, DiagnosticsHandler handler) {
+ assertFalse(tracedMethod.isMissingDefinition());
+ tracedMethods.add(tracedMethod.getReference());
+ }
+ }
+
+ private void runTest(Path sourceDex, TraceReferencesConsumer consumer) throws Exception {
+ testForTraceReferences()
+ .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.LATEST))
+ .addSourceFiles(sourceDex)
+ .addTargetClasses(
+ TargetAnnotationWithInt.class,
+ TargetAnnotationWithLongArray.class,
+ TargetAnnotationWithClassConstant.class,
+ TargetAnnotationWithClassConstantArray.class,
+ A.class,
+ B.class,
+ C.class,
+ D.class,
+ E.class,
+ F.class)
+ .setConsumer(consumer)
+ .trace();
+ }
+
+ private void test(Path sourceDex) throws Exception {
+ Consumer consumer = new Consumer();
+ runTest(sourceDex, consumer);
+ assertEquals(
+ ImmutableSet.of(
+ Reference.classFromClass(TargetAnnotationWithInt.class),
+ Reference.classFromClass(TargetAnnotationWithLongArray.class),
+ Reference.classFromClass(TargetAnnotationWithClassConstant.class),
+ Reference.classFromClass(TargetAnnotationWithClassConstantArray.class),
+ Reference.classFromClass(A.class),
+ Reference.classFromClass(B.class),
+ Reference.classFromClass(C.class),
+ Reference.classFromClass(D.class),
+ Reference.classFromClass(E.class),
+ Reference.classFromClass(F.class)),
+ consumer.tracedTypes);
+ }
+
+ private void testGeneratedKeepRules(Path sourceDex) throws Exception {
+ StringBuilder keepRulesBuilder = new StringBuilder();
+ runTest(
+ sourceDex,
+ TraceReferencesKeepRules.builder()
+ .setOutputConsumer((string, handler) -> keepRulesBuilder.append(string))
+ .build());
+ String expected =
+ StringUtils.lines(
+ "-keep class " + A.class.getTypeName() + " {",
+ "}",
+ "-keep class " + B.class.getTypeName() + " {",
+ "}",
+ "-keep class " + C.class.getTypeName() + " {",
+ "}",
+ "-keep class " + D.class.getTypeName() + " {",
+ "}",
+ "-keep class " + E.class.getTypeName() + " {",
+ "}",
+ "-keep class " + F.class.getTypeName() + " {",
+ "}",
+ "-keep @interface " + TargetAnnotationWithClassConstant.class.getTypeName() + " {",
+ " public java.lang.Class value();",
+ "}",
+ "-keep @interface " + TargetAnnotationWithClassConstantArray.class.getTypeName() + " {",
+ " public java.lang.Class[] value();",
+ "}",
+ "-keep @interface " + TargetAnnotationWithInt.class.getTypeName() + " {",
+ " public int value();",
+ "}",
+ "-keep @interface " + TargetAnnotationWithLongArray.class.getTypeName() + " {",
+ " public long[] value();",
+ "}");
+ assertEquals(expected, keepRulesBuilder.toString());
+ }
+
+ @Test
+ public void testDexArchive() throws Throwable {
+ Path archive = testForD8(Backend.DEX).addProgramClasses(SOURCE_CLASSES).compile().writeToZip();
+ test(archive);
+ testGeneratedKeepRules(archive);
+ }
+
+ @Test
+ public void testDexFile() throws Throwable {
+ Path dex =
+ testForD8(Backend.DEX)
+ .addProgramClasses(SOURCE_CLASSES)
+ .compile()
+ .writeToDirectory()
+ .resolve("classes.dex");
+ test(dex);
+ testGeneratedKeepRules(dex);
+ }
+
+ public class A {}
+
+ public class B {}
+
+ public class C {}
+
+ public class D {}
+
+ public class E {}
+
+ public class F {}
+
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface TargetAnnotationWithInt {
+ int value() default 0;
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface TargetAnnotationWithLongArray {
+ long[] value() default {0L, 1L};
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface TargetAnnotationWithClassConstant {
+ Class<?> value();
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface TargetAnnotationWithClassConstantArray {
+ Class<?>[] value();
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface SourceAnnotationWithClassConstant {
+ Class<?> value() default D.class;
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface SourceAnnotationWithClassConstantArray {
+ Class<?>[] value() default {E.class, F.class};
+ }
+
+ @TargetAnnotationWithInt(1)
+ @TargetAnnotationWithLongArray({2L, 3L})
+ @TargetAnnotationWithClassConstant(A.class)
+ @TargetAnnotationWithClassConstantArray({B.class, C.class})
+ @SourceAnnotationWithClassConstant
+ @SourceAnnotationWithClassConstantArray
+ static class Source {}
+}
diff --git a/src/test/testbase/java/com/android/tools/r8/TraceReferencesTestBuilder.java b/src/test/testbase/java/com/android/tools/r8/TraceReferencesTestBuilder.java
index d86cbbd..5291a77 100644
--- a/src/test/testbase/java/com/android/tools/r8/TraceReferencesTestBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/TraceReferencesTestBuilder.java
@@ -7,6 +7,7 @@
import com.android.tools.r8.tracereferences.TraceReferences;
import com.android.tools.r8.tracereferences.TraceReferencesCommand;
import com.android.tools.r8.tracereferences.TraceReferencesConsumer;
+import com.android.tools.r8.utils.ListUtils;
import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
import java.io.IOException;
import java.nio.file.Path;
@@ -79,6 +80,18 @@
return this;
}
+ static Collection<Path> getFilesForClasses(Collection<Class<?>> classes) {
+ return ListUtils.map(classes, ToolHelper::getClassFileForTestClass);
+ }
+
+ public TraceReferencesTestBuilder addTargetClasses(Class<?>... classes) {
+ return addTargetClasses(Arrays.asList(classes));
+ }
+
+ public TraceReferencesTestBuilder addTargetClasses(Collection<Class<?>> classes) {
+ return addTargetFiles(getFilesForClasses(classes));
+ }
+
public TraceReferencesTestResult trace() throws CompilationFailedException {
TraceReferences.run(builder.build());
return new TraceReferencesTestResult(inspector);