[KeepAnno] Support type specific annotation constraints

Fixes: b/319474935
Change-Id: Id297bd6b75352348484c938e5fa8ec1e9aafc2be
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepAnnotationCollectionInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepAnnotationCollectionInfo.java
index 203a006..144351c 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepAnnotationCollectionInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepAnnotationCollectionInfo.java
@@ -4,12 +4,15 @@
 
 package com.android.tools.r8.shaking;
 
-import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexAnnotation;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.utils.ObjectUtils;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
 public abstract class KeepAnnotationCollectionInfo {
 
@@ -258,6 +261,7 @@
       assert anyTypeInfo != null;
       assert anyTypeInfo.isAnyType();
       assert !anyTypeInfo.isTop() || specificTypeInfo == null;
+      assert specificTypeInfo == null || !specificTypeInfo.isEmpty();
       this.anyTypeInfo = anyTypeInfo;
       this.specificTypeInfo = specificTypeInfo;
     }
@@ -273,21 +277,40 @@
         return false;
       }
       if (specificTypeInfo != null) {
-        throw new Unimplemented();
+        KeepAnnotationInfo info = specificTypeInfo.get(annotation.getAnnotationType());
+        if (info != null) {
+          assert info.type.isIdenticalTo(annotation.getAnnotationType());
+          return !info.retention.matches(annotation);
+        }
       }
       return true;
     }
 
     public boolean internalIsLessThanOrEqualTo(IntermediateKeepAnnotationCollectionInfo other) {
-      if (specificTypeInfo == null && other.specificTypeInfo == null) {
-        return anyTypeInfo.isLessThanOrEqualTo(other.anyTypeInfo);
+      if (!anyTypeInfo.isLessThanOrEqualTo(other.anyTypeInfo)) {
+        return false;
       }
-      throw new Unimplemented();
+      if (specificTypeInfo == null) {
+        // Our specific types are "bottom" so this is less than.
+        return true;
+      }
+      if (other.specificTypeInfo == null) {
+        // Other specific types are "bottom" and this is not bottom, so it is not less than.
+        return false;
+      }
+      // Check that each specific type is less than the content of the type in other.
+      for (DexType type : specificTypeInfo.keySet()) {
+        KeepAnnotationInfo otherInfo = other.specificTypeInfo.get(type);
+        if (otherInfo == null || !specificTypeInfo.get(type).isLessThanOrEqualTo(otherInfo)) {
+          return false;
+        }
+      }
+      return true;
     }
   }
 
   public static Builder builder() {
-    return Builder.makeBottom();
+    return Builder.createBottom();
   }
 
   public Builder toBuilder() {
@@ -323,14 +346,20 @@
 
   public static class Builder {
 
-    public static Builder makeTop() {
+    public static Builder createTop() {
       return new Builder(KeepAnnotationInfo.getTop());
     }
 
-    public static Builder makeBottom() {
+    public static Builder createBottom() {
       return new Builder(KeepAnnotationInfo.getBottom());
     }
 
+    public void destructiveMakeTop() {
+      anyTypeInfo = KeepAnnotationInfo.getTop();
+      specificTypeInfo = null;
+      assert isTop();
+    }
+
     // Info applicable to any type.
     private KeepAnnotationInfo anyTypeInfo;
 
@@ -345,16 +374,16 @@
 
     private static Builder createFrom(KeepAnnotationCollectionInfo original) {
       if (original.isTop()) {
-        return makeTop();
+        return createTop();
       }
       if (original.isBottom()) {
-        return makeBottom();
+        return createBottom();
       }
       IntermediateKeepAnnotationCollectionInfo intermediate = original.asIntermediate();
-      Builder builder = makeBottom();
+      Builder builder = builder();
       builder.anyTypeInfo = intermediate.anyTypeInfo;
       if (intermediate.specificTypeInfo != null) {
-        throw new Unimplemented();
+        builder.specificTypeInfo = new IdentityHashMap<>(intermediate.specificTypeInfo);
       }
       return builder;
     }
@@ -368,42 +397,102 @@
     }
 
     public boolean isEqualTo(KeepAnnotationCollectionInfo other) {
-      // TODO(b/319474935): Consider checking directly on the builder and avoid the build.
-      KeepAnnotationCollectionInfo self = build();
-      return self.isLessThanOrEqualTo(other) && other.isLessThanOrEqualTo(self);
-    }
-
-    public Builder addItem(KeepAnnotationInfo item) {
-      if (item.isAnyType()) {
-        anyTypeInfo = anyTypeInfo.joinForSameType(item);
-        return this;
+      if (isBottom()) {
+        return other.isBottom();
       }
-      // TODO(b/319474935): Make sure to maintain the invariant that top => specific==null
-      throw new Unimplemented();
+      if (isTop()) {
+        return other.isTop();
+      }
+      IntermediateKeepAnnotationCollectionInfo intermediate = other.asIntermediate();
+      return anyTypeInfo.equals(intermediate.anyTypeInfo)
+          && Objects.equals(specificTypeInfo, intermediate.specificTypeInfo);
     }
 
-    public void join(Builder other) {
-      // Joining mutates 'this' with the join of settings from 'other'.
+    public void destructiveJoin(Builder other) {
       // The empty collection is bottom which joins as identity.
       if (other.isBottom()) {
         return;
       }
+      if (other.isTop()) {
+        destructiveMakeTop();
+        return;
+      }
+      // Always join the any-type info. If the any-type becomes top, then it applies to all
+      // specific types too, and we can simply update the info to top.
+      KeepAnnotationInfo oldAnyTypeInfo = anyTypeInfo;
       anyTypeInfo = anyTypeInfo.joinForSameType(other.anyTypeInfo);
-      if (specificTypeInfo != null || other.specificTypeInfo != null) {
-        throw new Unimplemented();
+      if (anyTypeInfo.isTop()) {
+        destructiveMakeTop();
+        return;
+      }
+      if (other.specificTypeInfo != null) {
+        if (specificTypeInfo == null) {
+          specificTypeInfo = new IdentityHashMap<>(other.specificTypeInfo);
+        } else {
+          other.specificTypeInfo.forEach(
+              (type, info) ->
+                  specificTypeInfo.compute(
+                      type,
+                      (t, existing) -> existing == null ? info : existing.joinForSameType(info)));
+        }
+        pruneSubsumedSpecificTypeInfos();
+      } else if (anyTypeInfo != oldAnyTypeInfo) {
+        pruneSubsumedSpecificTypeInfos();
       }
     }
 
-    public void joinAnyTypeInfo(RetentionInfo retention) {
-      // Joining mutates 'this' with the join of settings from 'retention'.
-      // The empty retention is bottom which joins as identity.
-      if (retention.isNone()) {
+    private void pruneSubsumedSpecificTypeInfos() {
+      if (specificTypeInfo == null) {
         return;
       }
-      anyTypeInfo = anyTypeInfo.joinForSameType(KeepAnnotationInfo.createForAnyType(retention));
-      if (specificTypeInfo != null) {
-        throw new Unimplemented();
+      assert !specificTypeInfo.isEmpty();
+      // If the any-type does not retain anything then nothing needs to be pruned.
+      if (anyTypeInfo.isBottom()) {
+        return;
       }
+      List<DexType> typesToPrune = null;
+      for (DexType type : specificTypeInfo.keySet()) {
+        KeepAnnotationInfo info = specificTypeInfo.get(type);
+        if (info.retention.isLessThanOrEqualTo(anyTypeInfo.retention)) {
+          if (typesToPrune == null) {
+            typesToPrune = new ArrayList<>(specificTypeInfo.size());
+          }
+          typesToPrune.add(type);
+        }
+      }
+      if (typesToPrune != null) {
+        if (typesToPrune.size() == specificTypeInfo.size()) {
+          specificTypeInfo = null;
+        } else {
+          typesToPrune.forEach(specificTypeInfo::remove);
+        }
+      }
+    }
+
+    public void destructiveJoinAnyTypeInfo(RetentionInfo retention) {
+      if (retention.isLessThanOrEqualTo(anyTypeInfo.retention)) {
+        return;
+      }
+      anyTypeInfo = KeepAnnotationInfo.createForAnyType(anyTypeInfo.retention.join(retention));
+      pruneSubsumedSpecificTypeInfos();
+    }
+
+    public void destructiveJoinTypeInfo(DexType type, RetentionInfo retention) {
+      assert type != null;
+      if (retention.isLessThanOrEqualTo(anyTypeInfo.retention)) {
+        return;
+      }
+      if (specificTypeInfo == null) {
+        // We expect the number of specific annotations to keep to be small, so we use a small
+        // initial capacity.
+        specificTypeInfo = new IdentityHashMap<>(3);
+      }
+      specificTypeInfo.compute(
+          type,
+          (t, prev) -> {
+            KeepAnnotationInfo info = new KeepAnnotationInfo(t, retention);
+            return prev == null ? info : info.joinForSameType(prev);
+          });
     }
 
     public KeepAnnotationCollectionInfo build() {
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepInfo.java
index 76afa92..4610887 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepInfo.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexAnnotation;
 import com.android.tools.r8.graph.DexReference;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.EnclosingMethodAttribute;
 import com.android.tools.r8.shaking.KeepAnnotationCollectionInfo.RetentionInfo;
 import com.android.tools.r8.shaking.KeepInfo.Builder;
@@ -492,25 +493,27 @@
       return setAllowAccessModificationForTesting(false);
     }
 
-    private B setAnnotationsInfo(KeepAnnotationCollectionInfo.Builder annotationsInfo) {
-      this.annotationsInfo = annotationsInfo;
-      return self();
-    }
-
     KeepAnnotationCollectionInfo.Builder getAnnotationsInfo() {
       return annotationsInfo;
     }
 
     public B allowAnnotationRemoval() {
-      return setAnnotationsInfo(KeepAnnotationCollectionInfo.Builder.makeBottom());
+      annotationsInfo = KeepAnnotationCollectionInfo.Builder.createBottom();
+      return self();
     }
 
     public B disallowAnnotationRemoval() {
-      return setAnnotationsInfo(KeepAnnotationCollectionInfo.Builder.makeTop());
+      annotationsInfo = KeepAnnotationCollectionInfo.Builder.createTop();
+      return self();
     }
 
     public B disallowAnnotationRemoval(RetentionInfo retention) {
-      annotationsInfo.joinAnyTypeInfo(retention);
+      annotationsInfo.destructiveJoinAnyTypeInfo(retention);
+      return self();
+    }
+
+    public B disallowAnnotationRemoval(RetentionInfo retention, DexType type) {
+      annotationsInfo.destructiveJoinTypeInfo(type, retention);
       return self();
     }
 
@@ -518,21 +521,18 @@
       return typeAnnotationsInfo;
     }
 
-    private B setTypeAnnotationsInfo(KeepAnnotationCollectionInfo.Builder typeAnnotationsInfo) {
-      this.typeAnnotationsInfo = typeAnnotationsInfo;
+    public B allowTypeAnnotationRemoval() {
+      typeAnnotationsInfo = KeepAnnotationCollectionInfo.Builder.createBottom();
       return self();
     }
 
-    public B allowTypeAnnotationRemoval() {
-      return setTypeAnnotationsInfo(KeepAnnotationCollectionInfo.Builder.makeBottom());
-    }
-
     public B disallowTypeAnnotationRemoval() {
-      return setTypeAnnotationsInfo(KeepAnnotationCollectionInfo.Builder.makeTop());
+      typeAnnotationsInfo = KeepAnnotationCollectionInfo.Builder.createTop();
+      return self();
     }
 
     public B disallowTypeAnnotationRemoval(RetentionInfo retention) {
-      typeAnnotationsInfo.joinAnyTypeInfo(retention);
+      typeAnnotationsInfo.destructiveJoinAnyTypeInfo(retention);
       return self();
     }
 
@@ -667,6 +667,11 @@
       return self();
     }
 
+    public J disallowAnnotationRemoval(RetentionInfo retention, DexType type) {
+      builder.disallowAnnotationRemoval(retention, type);
+      return self();
+    }
+
     public J disallowTypeAnnotationRemoval(RetentionInfo retention) {
       builder.disallowTypeAnnotationRemoval(retention);
       return self();
@@ -708,8 +713,8 @@
       applyIf(!otherBuilder.isShrinkingAllowed(), Joiner::disallowShrinking);
       applyIf(!otherBuilder.isSignatureRemovalAllowed(), Joiner::disallowSignatureRemoval);
       applyIf(otherBuilder.isCheckDiscardedEnabled(), Joiner::setCheckDiscarded);
-      builder.getAnnotationsInfo().join(otherBuilder.getAnnotationsInfo());
-      builder.getTypeAnnotationsInfo().join(otherBuilder.getTypeAnnotationsInfo());
+      builder.getAnnotationsInfo().destructiveJoin(otherBuilder.getAnnotationsInfo());
+      builder.getTypeAnnotationsInfo().destructiveJoin(otherBuilder.getTypeAnnotationsInfo());
       reasons.addAll(joiner.reasons);
       rules.addAll(joiner.rules);
       return self();
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepMethodInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepMethodInfo.java
index 42deba1..6b5a8a2 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepMethodInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepMethodInfo.java
@@ -535,17 +535,17 @@
     }
 
     public Builder allowParameterAnnotationsRemoval() {
-      parameterAnnotationsInfo = KeepAnnotationCollectionInfo.Builder.makeBottom();
+      parameterAnnotationsInfo = KeepAnnotationCollectionInfo.Builder.createBottom();
       return self();
     }
 
     public Builder disallowParameterAnnotationsRemoval() {
-      parameterAnnotationsInfo = KeepAnnotationCollectionInfo.Builder.makeTop();
+      parameterAnnotationsInfo = KeepAnnotationCollectionInfo.Builder.createTop();
       return self();
     }
 
     public Builder disallowParameterAnnotationsRemoval(RetentionInfo retention) {
-      parameterAnnotationsInfo.joinAnyTypeInfo(retention);
+      parameterAnnotationsInfo.destructiveJoinAnyTypeInfo(retention);
       return self();
     }
 
@@ -730,7 +730,9 @@
     public Joiner merge(Joiner joiner) {
       // Should be extended to merge the fields of this class in case any are added.
       super.merge(joiner);
-      builder.getParameterAnnotationsInfo().join(joiner.builder.getParameterAnnotationsInfo());
+      builder
+          .getParameterAnnotationsInfo()
+          .destructiveJoin(joiner.builder.getParameterAnnotationsInfo());
       return applyIf(!joiner.builder.isClassInliningAllowed(), Joiner::disallowClassInlining)
           .applyIf(
               !joiner.builder.isClosedWorldReasoningAllowed(), Joiner::disallowClosedWorldReasoning)
diff --git a/src/main/java/com/android/tools/r8/shaking/rules/KeepAnnotationMatcher.java b/src/main/java/com/android/tools/r8/shaking/rules/KeepAnnotationMatcher.java
index 1a341f7..1ef228f 100644
--- a/src/main/java/com/android/tools/r8/shaking/rules/KeepAnnotationMatcher.java
+++ b/src/main/java/com/android/tools/r8/shaking/rules/KeepAnnotationMatcher.java
@@ -88,12 +88,14 @@
                 result -> {
                   if (result.preconditions.isEmpty()) {
                     builder.addRootRule(
-                        keepInfoCollection -> createKeepInfo(result, keepInfoCollection));
+                        keepInfoCollection ->
+                            createKeepInfo(result, keepInfoCollection, predicates));
                   } else {
                     builder.addConditionalRule(
                         new PendingInitialConditionalRule(
                             result.preconditions,
-                            createKeepInfo(result, MinimumKeepInfoCollection.create())));
+                            createKeepInfo(
+                                result, MinimumKeepInfoCollection.create(), predicates)));
                   }
                 }),
         check -> {
@@ -102,19 +104,25 @@
   }
 
   private static MinimumKeepInfoCollection createKeepInfo(
-      MatchResult result, MinimumKeepInfoCollection minimumKeepInfoCollection) {
+      MatchResult result,
+      MinimumKeepInfoCollection minimumKeepInfoCollection,
+      KeepAnnotationMatcherPredicates predicates) {
     ListUtils.forEachWithIndex(
         result.consequences,
         (item, i) -> {
           Joiner<?, ?, ?> joiner =
               minimumKeepInfoCollection.getOrCreateMinimumKeepInfoFor(item.getReference());
-          updateWithConstraints(item, joiner, result.constraints.get(i), result.edge);
+          updateWithConstraints(item, joiner, result.constraints.get(i), result.edge, predicates);
         });
     return minimumKeepInfoCollection;
   }
 
   private static void updateWithConstraints(
-      ProgramDefinition item, Joiner<?, ?, ?> joiner, KeepConstraints constraints, KeepEdge edge) {
+      ProgramDefinition item,
+      Joiner<?, ?, ?> joiner,
+      KeepConstraints constraints,
+      KeepEdge edge,
+      KeepAnnotationMatcherPredicates predicates) {
     constraints.forEachAccept(
         new KeepConstraintVisitor() {
 
@@ -188,8 +196,15 @@
             if (pattern.getNamePattern().isAny()) {
               joiner.disallowAnnotationRemoval(toRetentionInfo(pattern));
             } else {
-              // TODO(b/319474935): Add to the type specific keep info.
-              joiner.disallowAnnotationRemoval(toRetentionInfo(pattern));
+              item.getDefinition()
+                  .annotations()
+                  .forEach(
+                      annotation -> {
+                        if (predicates.matchesAnnotation(annotation, pattern)) {
+                          joiner.disallowAnnotationRemoval(
+                              toRetentionInfo(pattern), annotation.getAnnotationType());
+                        }
+                      });
             }
           }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/rules/KeepAnnotationMatcherPredicates.java b/src/main/java/com/android/tools/r8/shaking/rules/KeepAnnotationMatcherPredicates.java
index b4ff8b8..b8862fe 100644
--- a/src/main/java/com/android/tools/r8/shaking/rules/KeepAnnotationMatcherPredicates.java
+++ b/src/main/java/com/android/tools/r8/shaking/rules/KeepAnnotationMatcherPredicates.java
@@ -20,6 +20,7 @@
 import com.android.tools.r8.graph.FieldAccessFlags;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.keepanno.ast.AccessVisibility;
+import com.android.tools.r8.keepanno.ast.KeepAnnotationPattern;
 import com.android.tools.r8.keepanno.ast.KeepArrayTypePattern;
 import com.android.tools.r8.keepanno.ast.KeepClassItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldAccessPattern;
@@ -302,4 +303,19 @@
     }
     return type.asPrimitiveTypeDescriptorChar() == pattern.getDescriptorChar();
   }
+
+  public boolean matchesAnnotation(DexAnnotation annotation, KeepAnnotationPattern pattern) {
+    int visibility = annotation.getVisibility();
+    if (visibility != DexAnnotation.VISIBILITY_BUILD
+        && visibility != DexAnnotation.VISIBILITY_RUNTIME) {
+      return false;
+    }
+    if (visibility == DexAnnotation.VISIBILITY_BUILD && !pattern.includesClassRetention()) {
+      return false;
+    }
+    if (visibility == DexAnnotation.VISIBILITY_RUNTIME && !pattern.includesRuntimeRetention()) {
+      return false;
+    }
+    return matchesClassName(annotation.getAnnotationType(), pattern.getNamePattern());
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/keepanno/ClassAnnotatedBySingleAnnotationTest.java b/src/test/java/com/android/tools/r8/keepanno/ClassAnnotatedBySingleAnnotationTest.java
new file mode 100644
index 0000000..b54350c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/ClassAnnotatedBySingleAnnotationTest.java
@@ -0,0 +1,124 @@
+// 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.keepanno;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsentIf;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.keepanno.annotations.AnnotationPattern;
+import com.android.tools.r8.keepanno.annotations.KeepConstraint;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.UsedByReflection;
+import com.android.tools.r8.keepanno.annotations.UsesReflection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+
+@RunWith(Parameterized.class)
+public class ClassAnnotatedBySingleAnnotationTest extends KeepAnnoTestBase {
+
+  static final String EXPECTED = StringUtils.lines("C1", "C2");
+
+  @Parameter public KeepAnnoParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static List<KeepAnnoParameters> data() {
+    return createParameters(
+        getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build());
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForKeepAnno(parameters)
+        .enableNativeInterpretation()
+        .addProgramClasses(getInputClasses())
+        .setExcludedOuterClass(getClass())
+        .run(TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .applyIf(parameters.isShrinker(), r -> r.inspect(this::checkOutput));
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(
+        TestClass.class, Reflector.class, A1.class, A2.class, C1.class, C2.class, C3.class);
+  }
+
+  private void checkOutput(CodeInspector inspector) {
+    // The class constant use will ensure both annotations remain.
+    assertThat(inspector.clazz(A1.class), isPresentAndRenamed());
+    assertThat(inspector.clazz(A2.class), isPresentAndRenamed());
+
+    // The last class will remain due to the class constant, but it is optimized/renamed.
+    assertThat(inspector.clazz(C3.class), isPresentAndRenamed());
+
+    ClassSubject c1 = inspector.clazz(C1.class);
+    ClassSubject c2 = inspector.clazz(C2.class);
+    // The A1 annotated classes must retain their name and annotation(s).
+    assertThat(c1, isPresentAndNotRenamed());
+    assertThat(c2, isPresentAndNotRenamed());
+
+    // In native mode, the restriction will only keep the annotation reference to A1, so C2 will
+    // have one annotation in native and two in other modes.
+    assertThat(c2.annotation(A1.class), isPresent());
+    assertThat(c2.annotation(A2.class), isAbsentIf(parameters.isNativeR8()));
+  }
+
+  @Target(ElementType.TYPE)
+  @Retention(RetentionPolicy.RUNTIME)
+  @interface A1 {}
+
+  @Target(ElementType.TYPE)
+  @Retention(RetentionPolicy.RUNTIME)
+  @interface A2 {}
+
+  static class Reflector {
+
+    @UsesReflection(
+        @KeepTarget(
+            classAnnotatedByClassConstant = A1.class,
+            constraints = KeepConstraint.NAME,
+            constrainAnnotations = @AnnotationPattern(constant = A1.class)))
+    public void foo(Class<?>... classes) throws Exception {
+      for (Class<?> clazz : classes) {
+        if (clazz.isAnnotationPresent(A1.class)) {
+          String typeName = clazz.getTypeName();
+          System.out.println(typeName.substring(typeName.lastIndexOf('$') + 1));
+        }
+      }
+    }
+  }
+
+  @A1
+  static class C1 {}
+
+  @A1
+  @A2
+  static class C2 {}
+
+  @A2
+  static class C3 {}
+
+  static class TestClass {
+
+    @UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS)
+    public static void main(String[] args) throws Exception {
+      new Reflector().foo(C1.class, C2.class, C3.class, A2.class);
+    }
+  }
+}