From 0469f887876b311a7e63a077c2f96133f0ee316c Mon Sep 17 00:00:00 2001 From: "Gerlach, Winfried" Date: Wed, 15 Jul 2026 19:23:08 +0200 Subject: [PATCH] #1382 make TYPE_USE annotations visible as dependencies --- .../archunit/core/domain/ImportContext.java | 2 + .../archunit/core/domain/JavaClass.java | 12 +++ .../core/domain/JavaClassDependencies.java | 11 +- .../core/importer/ClassFileImportRecord.java | 9 ++ .../core/importer/ClassFileProcessor.java | 6 ++ .../core/importer/ClassGraphCreator.java | 11 ++ .../core/importer/DeclarationHandler.java | 2 + .../core/importer/JavaClassProcessor.java | 33 ++++++ .../archunit/core/domain/JavaClassTest.java | 83 ++++++++++++++ .../core/importer/ImportTestUtils.java | 5 + type-use-annotations-summary.md | 102 ++++++++++++++++++ 11 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 type-use-annotations-summary.md diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/ImportContext.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/ImportContext.java index 681d0b8789..7ab8916f95 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/ImportContext.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/ImportContext.java @@ -47,6 +47,8 @@ public interface ImportContext { Map> createAnnotations(JavaMember owner); + Set> createTypeAnnotations(JavaClass owner); + Optional createEnclosingClass(JavaClass owner); Optional createEnclosingCodeUnit(JavaClass owner); diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java index 925fb84d12..42afa49379 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java @@ -68,6 +68,7 @@ import static java.util.Arrays.stream; import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; +import static java.util.Collections.emptySet; import static java.util.stream.Collectors.toSet; @PublicAPI(usage = ACCESS) @@ -126,6 +127,7 @@ public final class JavaClass private EnclosingDeclaration enclosingDeclaration = EnclosingDeclaration.ABSENT; private Optional componentType = Optional.empty(); private Map> annotations = emptyMap(); + private Set> typeAnnotations = emptySet(); private JavaClassDependencies javaClassDependencies = new JavaClassDependencies(this); // just for stubs; will be overwritten for imported classes private ReverseDependencies reverseDependencies = ReverseDependencies.EMPTY; // just for stubs; will be overwritten for imported classes private final CompletionProcess completionProcess; @@ -1480,10 +1482,20 @@ void completeMembers(ImportContext context) { void completeAnnotations(ImportContext context) { annotations = context.createAnnotations(this); + typeAnnotations = context.createTypeAnnotations(this); members.completeAnnotations(context); completionProcess.markAnnotationsComplete(); } + /** + * TYPE_USE annotations (JVMS §4.7.20) attributed to this class. Unlike {@link #getAnnotations() declaration + * annotations} these are not positioned within the annotated type; they are captured only so that the + * referenced annotation type surfaces as a {@link Dependency dependency} of this class. + */ + Set> getTypeAnnotations() { + return typeAnnotations; + } + JavaClassDependencies completeFrom(ImportContext context) { completeComponentType(context); members.completeFrom(context); diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDependencies.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDependencies.java index 7e415ea675..dc7aaf6eb4 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDependencies.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDependencies.java @@ -48,6 +48,7 @@ private Supplier> createDirectDependenciesFromClassSupplier() { codeUnitParameterDependenciesFromSelf(), throwsDeclarationDependenciesFromSelf(), annotationDependenciesFromSelf(), + typeAnnotationDependenciesFromSelf(), instanceofCheckDependenciesFromSelf(), referencedClassObjectDependenciesFromSelf(), typeParameterDependenciesFromSelf() @@ -246,9 +247,17 @@ private > Stream annota return annotatedObjects.stream().flatMap(this::annotationDependencies); } + private Stream typeAnnotationDependenciesFromSelf() { + return dependenciesOfAnnotations(javaClass.getTypeAnnotations()); + } + private > Stream annotationDependencies(T annotated) { + return dependenciesOfAnnotations(annotated.getAnnotations()); + } + + private Stream dependenciesOfAnnotations(Iterable> annotations) { Stream.Builder addToStream = Stream.builder(); - for (JavaAnnotation annotation : annotated.getAnnotations()) { + for (JavaAnnotation annotation : annotations) { Dependency.tryCreateFromAnnotation(annotation).forEach(addToStream); annotation.accept(new DefaultParameterVisitor() { @Override diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileImportRecord.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileImportRecord.java index e4c2610754..6b0f64496c 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileImportRecord.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileImportRecord.java @@ -75,6 +75,7 @@ class ClassFileImportRecord { private final SetMultimap constructorBuildersByOwner = HashMultimap.create(); private final Map staticInitializerBuildersByOwner = new HashMap<>(); private final SetMultimap annotationsByOwner = HashMultimap.create(); + private final SetMultimap typeAnnotationsByOwner = HashMultimap.create(); private final Map annotationDefaultValuesByOwner = new HashMap<>(); private final EnclosingDeclarationsByInnerClasses enclosingDeclarationsByOwner = new EnclosingDeclarationsByInnerClasses(); @@ -140,6 +141,10 @@ void addMemberAnnotations(String declaringClassName, String memberName, String d this.annotationsByOwner.putAll(getMemberKey(declaringClassName, memberName, descriptor), annotations); } + void addTypeAnnotations(String ownerName, Set annotations) { + this.typeAnnotationsByOwner.putAll(ownerName, annotations); + } + void addAnnotationDefaultValue(String declaringClassName, String methodName, String descriptor, JavaAnnotationBuilder.ValueBuilder valueBuilder) { annotationDefaultValuesByOwner.put(getMemberKey(declaringClassName, methodName, descriptor), valueBuilder); } @@ -203,6 +208,10 @@ Set getAnnotationsFor(JavaMember owner) { return annotationsByOwner.get(getMemberKey(owner)); } + Set getTypeAnnotationsFor(JavaClass owner) { + return typeAnnotationsByOwner.get(owner.getName()); + } + Optional getAnnotationDefaultValueBuilderFor(JavaMethod method) { return Optional.ofNullable(annotationDefaultValuesByOwner.get(getMemberKey(method))); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileProcessor.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileProcessor.java index d3fc55dc98..66ca034dba 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileProcessor.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileProcessor.java @@ -152,6 +152,12 @@ public void onDeclaredMemberAnnotations(String memberName, String descriptor, Se registerAnnotationTypesToResolve(annotationBuilders); } + @Override + public void onDeclaredTypeAnnotations(Set annotationBuilders) { + importRecord.addTypeAnnotations(ownerName, annotationBuilders); + registerAnnotationTypesToResolve(annotationBuilders); + } + private void registerAnnotationTypesToResolve(Set annotationBuilders) { for (JavaAnnotationBuilder annotationBuilder : annotationBuilders) { dependencyResolutionProcess.registerAnnotationType(annotationBuilder.getFullyQualifiedClassName()); diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassGraphCreator.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassGraphCreator.java index 93412f3c61..f57280d205 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassGraphCreator.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassGraphCreator.java @@ -358,6 +358,17 @@ private Map> create return buildAnnotations(owner, annotationBuilders, classes); } + @Override + public Set> createTypeAnnotations(JavaClass owner) { + // Unlike declaration annotations, several type-use annotations of the same type can be attributed to a + // single class (e.g. two @Nullable fields), so we collect them into a Set instead of a Map keyed by type. + ImmutableSet.Builder> result = ImmutableSet.builder(); + for (DomainBuilders.JavaAnnotationBuilder annotationBuilder : importRecord.getTypeAnnotationsFor(owner)) { + result.add(annotationBuilder.build(owner, classes)); + } + return result.build(); + } + @Override public Optional createEnclosingClass(JavaClass owner) { Optional enclosingClassName = importRecord.getEnclosingClassFor(owner.getName()); diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/DeclarationHandler.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/DeclarationHandler.java index d772ff3d9b..b2ecdb3aec 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/DeclarationHandler.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/DeclarationHandler.java @@ -45,6 +45,8 @@ interface DeclarationHandler { void onDeclaredMemberAnnotations(String memberName, String descriptor, Set annotations); + void onDeclaredTypeAnnotations(Set annotations); + void onDeclaredAnnotationValueType(String valueTypeName); void onDeclaredAnnotationDefaultValue(String methodName, String methodDescriptor, DomainBuilders.JavaAnnotationBuilder.ValueBuilder valueBuilder); diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaClassProcessor.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaClassProcessor.java index d2bcdb715b..1ad394b47d 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaClassProcessor.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaClassProcessor.java @@ -57,6 +57,7 @@ import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; +import org.objectweb.asm.TypePath; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -79,6 +80,7 @@ class JavaClassProcessor extends ClassVisitor { private DomainBuilders.JavaClassBuilder javaClassBuilder; private final Set annotations = new HashSet<>(); + private final Set typeAnnotations = new HashSet<>(); private final SourceDescriptor sourceDescriptor; private final DeclarationHandler declarationHandler; private final AccessHandler accessHandler; @@ -300,6 +302,18 @@ public AnnotationVisitor visitAnnotation(String desc, boolean visible) { return new AnnotationProcessor(annotations::add, declarationHandler, handleAnnotationAnnotationProperty(desc, declarationHandler)); } + // Type annotations (JVMS §4.7.20, e.g. Checker Framework's TYPE_USE @Nullable) are stored in a separate + // attribute from declaration annotations. We don't model their exact position within a type, but we do + // capture the referenced annotation type (and its members) so that it surfaces as a dependency of this class. + @Override + public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String desc, boolean visible) { + if (importAborted()) { + return super.visitTypeAnnotation(typeRef, typePath, desc, visible); + } + + return new AnnotationProcessor(typeAnnotations::add, declarationHandler, handleAnnotationAnnotationProperty(desc, declarationHandler)); + } + @Override public void visitEnd() { if (importAborted()) { @@ -307,6 +321,7 @@ public void visitEnd() { } declarationHandler.onDeclaredClassAnnotations(annotations); + declarationHandler.onDeclaredTypeAnnotations(typeAnnotations); LOG.trace("Done analyzing {}", className); } @@ -316,6 +331,7 @@ private static class MethodProcessor extends MethodVisitor { private final DomainBuilders.JavaCodeUnitBuilder codeUnitBuilder; private final DeclarationHandler declarationHandler; private final Set annotations = new HashSet<>(); + private final Set typeAnnotations = new HashSet<>(); private final SetMultimap parameterAnnotationsByIndex = HashMultimap.create(); private int actualLineNumber; @@ -395,6 +411,13 @@ public AnnotationVisitor visitAnnotation(String desc, boolean visible) { return new AnnotationProcessor(annotations::add, declarationHandler, handleAnnotationAnnotationProperty(desc, declarationHandler)); } + // Captures TYPE_USE annotations on the method signature (return type, formal parameter types, + // throws types, type parameters). We attribute them to the declaring class as a dependency. + @Override + public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String desc, boolean visible) { + return new AnnotationProcessor(typeAnnotations::add, declarationHandler, handleAnnotationAnnotationProperty(desc, declarationHandler)); + } + @Override public AnnotationVisitor visitAnnotationDefault() { return new AnnotationDefaultProcessor(declaringClassName, codeUnitBuilder, declarationHandler); @@ -421,6 +444,7 @@ private void processLambdaMetafactoryMethodHandleArgument(Handle methodHandle) { @Override public void visitEnd() { declarationHandler.onDeclaredMemberAnnotations(codeUnitBuilder.getName(), codeUnitBuilder.getDescriptor(), annotations); + declarationHandler.onDeclaredTypeAnnotations(typeAnnotations); accessHandler.onMethodEnd(); } @@ -585,6 +609,7 @@ private static class FieldProcessor extends FieldVisitor { private final DomainBuilders.JavaFieldBuilder fieldBuilder; private final DeclarationHandler declarationHandler; private final Set annotations = new HashSet<>(); + private final Set typeAnnotations = new HashSet<>(); private FieldProcessor(DomainBuilders.JavaFieldBuilder fieldBuilder, DeclarationHandler declarationHandler) { super(ASM_API_VERSION); @@ -598,9 +623,17 @@ public AnnotationVisitor visitAnnotation(String desc, boolean visible) { return new AnnotationProcessor(annotations::add, declarationHandler, handleAnnotationAnnotationProperty(desc, declarationHandler)); } + // Captures TYPE_USE annotations on the field type (e.g. @Nullable String field), attributed to the + // declaring class as a dependency. + @Override + public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String desc, boolean visible) { + return new AnnotationProcessor(typeAnnotations::add, declarationHandler, handleAnnotationAnnotationProperty(desc, declarationHandler)); + } + @Override public void visitEnd() { declarationHandler.onDeclaredMemberAnnotations(fieldBuilder.getName(), fieldBuilder.getDescriptor(), annotations); + declarationHandler.onDeclaredTypeAnnotations(typeAnnotations); } } diff --git a/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaClassTest.java b/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaClassTest.java index 52f9064600..59aa155ca0 100644 --- a/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaClassTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaClassTest.java @@ -5,6 +5,7 @@ import java.io.FilterInputStream; import java.io.Serializable; import java.lang.annotation.Retention; +import java.lang.annotation.Target; import java.nio.Buffer; import java.nio.charset.Charset; import java.nio.file.FileSystem; @@ -99,6 +100,7 @@ import static com.tngtech.archunit.testutil.Conditions.containing; import static com.tngtech.archunit.testutil.ReflectionTestUtils.getHierarchy; import static com.tngtech.archunit.testutil.assertion.DependenciesAssertion.from; +import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.RetentionPolicy.RUNTIME; import static java.util.Collections.singletonList; import static java.util.regex.Pattern.quote; @@ -862,6 +864,38 @@ public void direct_dependencies_from_self_by_annotation() { .inLineNumber(0)); } + @Test + public void direct_dependencies_from_self_by_type_annotation() { + JavaClass javaClass = importClasses(ClassWithTypeAnnotationDependencies.class) + .get(ClassWithTypeAnnotationDependencies.class); + + assertThat(javaClass.getDirectDependenciesFromSelf()) + .areAtLeastOne(annotationTypeDependency() + .from(ClassWithTypeAnnotationDependencies.class) + .to(TypeUseAnnotationOnField.class) + .inLineNumber(0)) + .areAtLeastOne(annotationTypeDependency() + .from(ClassWithTypeAnnotationDependencies.class) + .to(TypeUseAnnotationOnTypeArgument.class) + .inLineNumber(0)) + .areAtLeastOne(annotationTypeDependency() + .from(ClassWithTypeAnnotationDependencies.class) + .to(TypeUseAnnotationOnReturnType.class) + .inLineNumber(0)) + .areAtLeastOne(annotationTypeDependency() + .from(ClassWithTypeAnnotationDependencies.class) + .to(TypeUseAnnotationOnParameterType.class) + .inLineNumber(0)) + .areAtLeastOne(annotationTypeDependency() + .from(ClassWithTypeAnnotationDependencies.class) + .to(TypeUseAnnotationWithMember.class) + .inLineNumber(0)) + .areAtLeastOne(annotationMemberOfTypeDependency() + .from(ClassWithTypeAnnotationDependencies.class) + .to(TypeAnnotationMemberType.class) + .inLineNumber(0)); + } + @Test public void finds_array_component_types_as_dependencies_from_self() { JavaClass javaClass = importClassWithContext(ArrayComponentTypeDependencies.class); @@ -2511,6 +2545,55 @@ void method(@OnMethodParam String param) { } } + @SuppressWarnings("unused") + private static class ClassWithTypeAnnotationDependencies { + @TypeUseAnnotationOnField + List<@TypeUseAnnotationOnTypeArgument TypeAnnotatedType> field; + + @TypeUseAnnotationOnReturnType + TypeAnnotatedType method() { + return null; + } + + void method(@TypeUseAnnotationOnParameterType TypeAnnotatedType param) { + } + + void methodWithAnnotationMember(@TypeUseAnnotationWithMember(TypeAnnotationMemberType.class) TypeAnnotatedType param) { + } + } + + @Retention(RUNTIME) + @Target(TYPE_USE) + private @interface TypeUseAnnotationOnField { + } + + @Retention(RUNTIME) + @Target(TYPE_USE) + private @interface TypeUseAnnotationOnTypeArgument { + } + + @Retention(RUNTIME) + @Target(TYPE_USE) + private @interface TypeUseAnnotationOnReturnType { + } + + @Retention(RUNTIME) + @Target(TYPE_USE) + private @interface TypeUseAnnotationOnParameterType { + } + + @Retention(RUNTIME) + @Target(TYPE_USE) + private @interface TypeUseAnnotationWithMember { + Class value(); + } + + private static class TypeAnnotatedType { + } + + private static class TypeAnnotationMemberType { + } + private @interface OnClass { } diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/ImportTestUtils.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/ImportTestUtils.java index 5a31d7f33a..59bac8b88c 100644 --- a/archunit/src/test/java/com/tngtech/archunit/core/importer/ImportTestUtils.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/importer/ImportTestUtils.java @@ -391,6 +391,11 @@ public Map> createAnnotations(JavaMember owne return emptyMap(); } + @Override + public Set> createTypeAnnotations(JavaClass owner) { + return Collections.emptySet(); + } + @Override public Optional createEnclosingClass(JavaClass owner) { return Optional.empty(); diff --git a/type-use-annotations-summary.md b/type-use-annotations-summary.md new file mode 100644 index 0000000000..010d5e2b05 --- /dev/null +++ b/type-use-annotations-summary.md @@ -0,0 +1,102 @@ +# Making TYPE_USE annotations visible as dependencies + +## Problem + +Annotations declared with `@Target(TYPE_USE)` (or `TYPE_PARAMETER`) only — the most +prominent example being Checker Framework's `@Nullable` — are invisible to ArchUnit. + +Because such an annotation has no `FIELD`/`METHOD`/`PARAMETER` target, `javac` does **not** +write it into the normal declaration-annotation tables (`RuntimeVisible/InvisibleAnnotations`). +It writes it into the separate type-annotation table (`RuntimeVisibleTypeAnnotations`, +JVMS §4.7.20). ArchUnit's importer never read that table, so: + +- `javaClass.getDirectDependenciesFromSelf()` never contained the annotation type, and +- `dependOnClassesThat().resideInAPackage("org.checkerframework.checker.nullness.qual")` + found nothing. + +## Scope of this change ("the moderate path") + +Make TYPE_USE annotations surface as **dependencies of the enclosing class**. This is the +smallest, self-contained increment that fixes the reported problem. + +It deliberately does **not** attach the annotation to a specific type-usage position +(e.g. `@Nullable` on _this field's_ type vs. _that type argument_). Full positional modeling +would require decoding ASM's `TypeReference`/`TypePath` and introducing annotated-type nodes +across ~17 positions — a much larger feature, out of scope here. + +## How it works + +The data flow mirrors how declaration annotations are already handled: + +1. **ASM capture** — `JavaClassProcessor` now overrides + `visitTypeAnnotation(int typeRef, TypePath typePath, String desc, boolean visible)` on the + class visitor, `FieldProcessor`, and `MethodProcessor`. Each reuses the existing + `AnnotationProcessor`, so annotation **values** (e.g. `@Foo(Bar.class)`) are parsed too. + This covers field types, method return / formal-parameter / throws / type-parameter + positions, and nested positions such as `List<@X SomeType>`. +2. **Storage / wiring** — a new `DeclarationHandler.onDeclaredTypeAnnotations(...)` callback, + implemented by `ClassFileProcessor.ClassDetailsRecorder` (attributes to the class currently + being processed and registers the annotation types for resolution), stores the builders + per class in `ClassFileImportRecord` (`typeAnnotationsByOwner`). +3. **Model build** — a new `ImportContext.createTypeAnnotations(JavaClass)` method, implemented + in `ClassGraphCreator`, builds real `JavaAnnotation` objects (owner = the class) + into a `Set`, populated on `JavaClass` during `completeAnnotations`. +4. **Dependency derivation** — `JavaClassDependencies` gains a `typeAnnotationDependenciesFromSelf()` + stream that reuses the existing annotation-dependency logic (`Dependency.tryCreateFromAnnotation` + plus the member visitor). So both the annotation type itself and any class/enum members it + references become dependencies, with origin = the annotated class. + +## Files changed + +Production (`archunit/src/main/java`): + +| File | Change | +|------|--------| +| `core/importer/JavaClassProcessor.java` | Override `visitTypeAnnotation` on class/field/method visitors; collect per-processor and emit at `visitEnd`. Import `org.objectweb.asm.TypePath`. | +| `core/importer/DeclarationHandler.java` | New `onDeclaredTypeAnnotations(Set)`. | +| `core/importer/ClassFileProcessor.java` | Implement `onDeclaredTypeAnnotations` (store + register types to resolve). | +| `core/importer/ClassFileImportRecord.java` | `typeAnnotationsByOwner` storage + `addTypeAnnotations` / `getTypeAnnotationsFor`. | +| `core/domain/ImportContext.java` | New `createTypeAnnotations(JavaClass)`. | +| `core/importer/ClassGraphCreator.java` | Implement `createTypeAnnotations` (build a `Set>`). | +| `core/domain/JavaClass.java` | `typeAnnotations` field, populate in `completeAnnotations`, package-private `getTypeAnnotations()`. | +| `core/domain/JavaClassDependencies.java` | Refactor shared `dependenciesOfAnnotations(...)`; add `typeAnnotationDependenciesFromSelf()` to the dependency set. | + +Tests (`archunit/src/test/java`): + +| File | Change | +|------|--------| +| `core/domain/JavaClassTest.java` | New test `direct_dependencies_from_self_by_type_annotation` + TYPE_USE example classes/annotations. | +| `core/importer/ImportTestUtils.java` | Implement `createTypeAnnotations` on the `ImportContextStub`. | + +## Design decisions + +- **Class-level granularity.** Every type-use annotation is attributed to its enclosing class, + not to an exact type node. `TypePath`/`TypeReference` are intentionally ignored. +- **No new public API.** `JavaClass.getTypeAnnotations()` is package-private (used only for + dependency derivation), so there is no `@PublicAPI` surface change. Exposing a public + accessor to *query* type-use annotations would be a natural follow-up. +- **`Set`, not `Map`.** Several type-use annotations of the same type can attach to one class + (e.g. two `@Nullable` fields), so a `Set` is built directly rather than the + map-keyed-by-type produced by `buildAnnotations`. +- **Both retention kinds captured.** Like the existing `visitAnnotation`, the `visible` flag is + ignored, so both `RuntimeVisible-` and `RuntimeInvisibleTypeAnnotations` are read. (Checker's + `@Nullable` is `RUNTIME`.) + +## Verification + +- New test `JavaClassTest.direct_dependencies_from_self_by_type_annotation` passes. It uses + `@Target(TYPE_USE)`-only annotations on a field type, a type argument, a method return type, + a parameter type, and an annotation-with-class-member, and asserts each appears in + `getDirectDependenciesFromSelf()` (annotation type + annotation-member dependencies). +- Full regression: `JavaClassTest` (110 tests) and `ClassFileImporterAnnotationsTest` (26 tests) + both pass with 0 failures / 0 errors. +- `./gradlew :archunit:compileTestJava` compiles cleanly. + +## Not covered (possible follow-ups) + +- Type-use annotations inside **method bodies** (local variables, casts, `instanceof`, `new`) via + `visitInsnAnnotation` / `visitLocalVariableAnnotation` / `visitTryCatchAnnotation`. +- **Record components** (`visitRecordComponent().visitTypeAnnotation`) — currently covered only + indirectly via the generated field/accessor. +- A **public** `getTypeAnnotations()` accessor and/or **positional** modeling of type-use + annotations on the type nodes themselves. \ No newline at end of file