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