Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ public interface ImportContext {

Map<String, JavaAnnotation<JavaMember>> createAnnotations(JavaMember owner);

Set<JavaAnnotation<JavaClass>> createTypeAnnotations(JavaClass owner);

Optional<JavaClass> createEnclosingClass(JavaClass owner);

Optional<JavaCodeUnit> createEnclosingCodeUnit(JavaClass owner);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -126,6 +127,7 @@ public final class JavaClass
private EnclosingDeclaration enclosingDeclaration = EnclosingDeclaration.ABSENT;
private Optional<JavaClass> componentType = Optional.empty();
private Map<String, JavaAnnotation<JavaClass>> annotations = emptyMap();
private Set<JavaAnnotation<JavaClass>> 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;
Expand Down Expand Up @@ -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<JavaAnnotation<JavaClass>> getTypeAnnotations() {
return typeAnnotations;
}

JavaClassDependencies completeFrom(ImportContext context) {
completeComponentType(context);
members.completeFrom(context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ private Supplier<Set<Dependency>> createDirectDependenciesFromClassSupplier() {
codeUnitParameterDependenciesFromSelf(),
throwsDeclarationDependenciesFromSelf(),
annotationDependenciesFromSelf(),
typeAnnotationDependenciesFromSelf(),
instanceofCheckDependenciesFromSelf(),
referencedClassObjectDependenciesFromSelf(),
typeParameterDependenciesFromSelf()
Expand Down Expand Up @@ -246,9 +247,17 @@ private <T extends HasDescription & HasAnnotations<?>> Stream<Dependency> annota
return annotatedObjects.stream().flatMap(this::annotationDependencies);
}

private Stream<Dependency> typeAnnotationDependenciesFromSelf() {
return dependenciesOfAnnotations(javaClass.getTypeAnnotations());
}

private <T extends HasDescription & HasAnnotations<?>> Stream<Dependency> annotationDependencies(T annotated) {
return dependenciesOfAnnotations(annotated.getAnnotations());
}

private Stream<Dependency> dependenciesOfAnnotations(Iterable<? extends JavaAnnotation<?>> annotations) {
Stream.Builder<Dependency> addToStream = Stream.builder();
for (JavaAnnotation<?> annotation : annotated.getAnnotations()) {
for (JavaAnnotation<?> annotation : annotations) {
Dependency.tryCreateFromAnnotation(annotation).forEach(addToStream);
annotation.accept(new DefaultParameterVisitor() {
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class ClassFileImportRecord {
private final SetMultimap<String, JavaConstructorBuilder> constructorBuildersByOwner = HashMultimap.create();
private final Map<String, JavaStaticInitializerBuilder> staticInitializerBuildersByOwner = new HashMap<>();
private final SetMultimap<String, JavaAnnotationBuilder> annotationsByOwner = HashMultimap.create();
private final SetMultimap<String, JavaAnnotationBuilder> typeAnnotationsByOwner = HashMultimap.create();
private final Map<String, JavaAnnotationBuilder.ValueBuilder> annotationDefaultValuesByOwner = new HashMap<>();
private final EnclosingDeclarationsByInnerClasses enclosingDeclarationsByOwner = new EnclosingDeclarationsByInnerClasses();

Expand Down Expand Up @@ -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<JavaAnnotationBuilder> annotations) {
this.typeAnnotationsByOwner.putAll(ownerName, annotations);
}

void addAnnotationDefaultValue(String declaringClassName, String methodName, String descriptor, JavaAnnotationBuilder.ValueBuilder valueBuilder) {
annotationDefaultValuesByOwner.put(getMemberKey(declaringClassName, methodName, descriptor), valueBuilder);
}
Expand Down Expand Up @@ -203,6 +208,10 @@ Set<JavaAnnotationBuilder> getAnnotationsFor(JavaMember owner) {
return annotationsByOwner.get(getMemberKey(owner));
}

Set<JavaAnnotationBuilder> getTypeAnnotationsFor(JavaClass owner) {
return typeAnnotationsByOwner.get(owner.getName());
}

Optional<JavaAnnotationBuilder.ValueBuilder> getAnnotationDefaultValueBuilderFor(JavaMethod method) {
return Optional.ofNullable(annotationDefaultValuesByOwner.get(getMemberKey(method)));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ public void onDeclaredMemberAnnotations(String memberName, String descriptor, Se
registerAnnotationTypesToResolve(annotationBuilders);
}

@Override
public void onDeclaredTypeAnnotations(Set<JavaAnnotationBuilder> annotationBuilders) {
importRecord.addTypeAnnotations(ownerName, annotationBuilders);
registerAnnotationTypesToResolve(annotationBuilders);
}

private void registerAnnotationTypesToResolve(Set<JavaAnnotationBuilder> annotationBuilders) {
for (JavaAnnotationBuilder annotationBuilder : annotationBuilders) {
dependencyResolutionProcess.registerAnnotationType(annotationBuilder.getFullyQualifiedClassName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,17 @@ private <OWNER extends HasDescription> Map<String, JavaAnnotation<OWNER>> create
return buildAnnotations(owner, annotationBuilders, classes);
}

@Override
public Set<JavaAnnotation<JavaClass>> 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<JavaAnnotation<JavaClass>> result = ImmutableSet.builder();
for (DomainBuilders.JavaAnnotationBuilder annotationBuilder : importRecord.getTypeAnnotationsFor(owner)) {
result.add(annotationBuilder.build(owner, classes));
}
return result.build();
}

@Override
public Optional<JavaClass> createEnclosingClass(JavaClass owner) {
Optional<String> enclosingClassName = importRecord.getEnclosingClassFor(owner.getName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ interface DeclarationHandler {

void onDeclaredMemberAnnotations(String memberName, String descriptor, Set<DomainBuilders.JavaAnnotationBuilder> annotations);

void onDeclaredTypeAnnotations(Set<DomainBuilders.JavaAnnotationBuilder> annotations);

void onDeclaredAnnotationValueType(String valueTypeName);

void onDeclaredAnnotationDefaultValue(String methodName, String methodDescriptor, DomainBuilders.JavaAnnotationBuilder.ValueBuilder valueBuilder);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -79,6 +80,7 @@ class JavaClassProcessor extends ClassVisitor {

private DomainBuilders.JavaClassBuilder javaClassBuilder;
private final Set<JavaAnnotationBuilder> annotations = new HashSet<>();
private final Set<JavaAnnotationBuilder> typeAnnotations = new HashSet<>();
private final SourceDescriptor sourceDescriptor;
private final DeclarationHandler declarationHandler;
private final AccessHandler accessHandler;
Expand Down Expand Up @@ -300,13 +302,26 @@ 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()) {
return;
}

declarationHandler.onDeclaredClassAnnotations(annotations);
declarationHandler.onDeclaredTypeAnnotations(typeAnnotations);
LOG.trace("Done analyzing {}", className);
}

Expand All @@ -316,6 +331,7 @@ private static class MethodProcessor extends MethodVisitor {
private final DomainBuilders.JavaCodeUnitBuilder<?, ?> codeUnitBuilder;
private final DeclarationHandler declarationHandler;
private final Set<JavaAnnotationBuilder> annotations = new HashSet<>();
private final Set<JavaAnnotationBuilder> typeAnnotations = new HashSet<>();
private final SetMultimap<Integer, JavaAnnotationBuilder> parameterAnnotationsByIndex = HashMultimap.create();
private int actualLineNumber;

Expand Down Expand Up @@ -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);
Expand All @@ -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();
}

Expand Down Expand Up @@ -585,6 +609,7 @@ private static class FieldProcessor extends FieldVisitor {
private final DomainBuilders.JavaFieldBuilder fieldBuilder;
private final DeclarationHandler declarationHandler;
private final Set<JavaAnnotationBuilder> annotations = new HashSet<>();
private final Set<JavaAnnotationBuilder> typeAnnotations = new HashSet<>();

private FieldProcessor(DomainBuilders.JavaFieldBuilder fieldBuilder, DeclarationHandler declarationHandler) {
super(ASM_API_VERSION);
Expand All @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,11 @@ public Map<String, JavaAnnotation<JavaMember>> createAnnotations(JavaMember owne
return emptyMap();
}

@Override
public Set<JavaAnnotation<JavaClass>> createTypeAnnotations(JavaClass owner) {
return Collections.emptySet();
}

@Override
public Optional<JavaClass> createEnclosingClass(JavaClass owner) {
return Optional.empty();
Expand Down
Loading