[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);