Skip to content

Commit

Permalink
Review: To be consistent with other places of ArchUnit, the "owner" o…
Browse files Browse the repository at this point in the history
…f an annotation should be the closest "parent" where the annotation is declared. Like a ThrowsDeclaration on a method having that method as owner, an annotation on a method should as well. Likewise, if an annotation is a parameter of another annotation, it should still have that annotation as "owner". That way it is possible to traverse the declaration tree up and do assertions based on the specific case. Unfortunately for the dependency use case, this is a little less convenient, since we're interested in the origin class of the dependency, which now must be determined by traversal, but still the implementation of HasOwner should not be adjusted to a specific need in one place.

Issue: #136
Signed-off-by: Peter Gafert <peter.gafert@tngtech.com>
  • Loading branch information
codecholeric authored and Alen Kosanović committed Jan 9, 2020
1 parent 6116525 commit edb98da
Show file tree
Hide file tree
Showing 50 changed files with 679 additions and 374 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -306,10 +306,10 @@ public boolean apply(JavaClass input) {
};
}

private static DescribedPredicate<JavaAnnotation> publicApiForInheritance() {
return new DescribedPredicate<JavaAnnotation>("@%s(usage = %s)", PublicAPI.class.getSimpleName(), INHERITANCE) {
private static DescribedPredicate<JavaAnnotation<?>> publicApiForInheritance() {
return new DescribedPredicate<JavaAnnotation<?>>("@%s(usage = %s)", PublicAPI.class.getSimpleName(), INHERITANCE) {
@Override
public boolean apply(JavaAnnotation input) {
public boolean apply(JavaAnnotation<?> input) {
return input.getRawType().isEquivalentTo(PublicAPI.class) &&
input.as(PublicAPI.class).usage() == INHERITANCE;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
@SuppressWarnings("unchecked")
class TestAnalysisRequest implements ClassAnalysisRequest {
private String[] packages = new String[0];
private Class<?>[] packageRoots = new Class[0];
private Class<?>[] packageRoots = new Class<?>[0];
private Class<? extends LocationProvider>[] locationProviders = new Class[0];
private Class<? extends ImportOption>[] importOptions = new Class[0];
private CacheMode cacheMode = CacheMode.FOREVER;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ public boolean apply(JavaMember input) {
*/
@Override
@PublicAPI(usage = ACCESS)
public boolean isAnnotatedWith(final DescribedPredicate<? super JavaAnnotation> predicate) {
public boolean isAnnotatedWith(final DescribedPredicate<? super JavaAnnotation<?>> predicate) {
return anyMember(new Predicate<JavaMember>() {
@Override
public boolean apply(JavaMember input) {
Expand Down Expand Up @@ -202,7 +202,7 @@ public boolean apply(JavaMember input) {

@Override
@PublicAPI(usage = ACCESS)
public boolean isMetaAnnotatedWith(final DescribedPredicate<? super JavaAnnotation> predicate) {
public boolean isMetaAnnotatedWith(final DescribedPredicate<? super JavaAnnotation<?>> predicate) {
return anyMember(new Predicate<JavaMember>() {
@Override
public boolean apply(JavaMember input) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class AnnotationProxy {
DomainPlugin.Loader.loadForCurrentPlatform().plugInAnnotationValueFormatter(valueFormatter);
}

public static <A extends Annotation> A of(Class<A> annotationType, JavaAnnotation toProxy) {
public static <A extends Annotation> A of(Class<A> annotationType, JavaAnnotation<?> toProxy) {
checkArgument(annotationType.getName().equals(toProxy.getRawType().getName()),
"Requested annotation type %s is incompatible with %s of type %s",
annotationType.getSimpleName(), JavaAnnotation.class.getSimpleName(), toProxy.getRawType().getSimpleName());
Expand All @@ -54,19 +54,19 @@ public static <A extends Annotation> A of(Class<A> annotationType, JavaAnnotatio
}

@SuppressWarnings("unchecked") // annotationType A will be implemented
private static <A extends Annotation> A newProxy(Class<A> annotationType, JavaAnnotation toProxy) {
private static <A extends Annotation> A newProxy(Class<A> annotationType, JavaAnnotation<?> toProxy) {
return (A) Proxy.newProxyInstance(
annotationType.getClassLoader(),
new Class[]{annotationType},
new AnnotationMethodInvocationHandler(annotationType, toProxy));
}

private static class AnnotationMethodInvocationHandler implements InvocationHandler {
private final JavaAnnotation toProxy;
private final JavaAnnotation<?> toProxy;
private final Conversions conversions;
private final Map<MethodKey, SpecificHandler> handlersByMethod;

private AnnotationMethodInvocationHandler(Class<?> annotationType, JavaAnnotation toProxy) {
private AnnotationMethodInvocationHandler(Class<?> annotationType, JavaAnnotation<?> toProxy) {
this.toProxy = toProxy;
conversions = initConversions(annotationType);
handlersByMethod = initHandlersByMethod(annotationType, toProxy, conversions);
Expand All @@ -86,7 +86,7 @@ private Conversions initConversions(Class<?> annotationType) {
}

private ImmutableMap<MethodKey, SpecificHandler> initHandlersByMethod(
Class<?> annotationType, JavaAnnotation toProxy, Conversions conversions) {
Class<?> annotationType, JavaAnnotation<?> toProxy, Conversions conversions) {
return ImmutableMap.of(
new MethodKey("annotationType"), new ConstantReturnValueHandler(annotationType),
new MethodKey("equals", Object.class.getName()), new EqualsHandler(),
Expand Down Expand Up @@ -186,16 +186,16 @@ public boolean canHandle(Class<?> returnType) {
}
}

private static class JavaAnnotationConversion implements Conversion<JavaAnnotation> {
private static class JavaAnnotationConversion implements Conversion<JavaAnnotation<?>> {
private final ClassLoader classLoader;

private JavaAnnotationConversion(ClassLoader classLoader) {
this.classLoader = classLoader;
}

@Override
public Annotation convert(JavaAnnotation input, Class<?> returnType) {
// JavaAnnotation#getType() will return the type name of a Class<? extends Annotation>
public Annotation convert(JavaAnnotation<?> input, Class<?> returnType) {
// JavaAnnotation.getType() will return the type name of a Class<? extends Annotation>
@SuppressWarnings("unchecked")
Class<? extends Annotation> type = (Class<? extends Annotation>)
JavaType.From.javaClass(input.getRawType()).resolveClass(classLoader);
Expand Down Expand Up @@ -268,10 +268,10 @@ public Object handle(Object proxy, Method method, Object[] args) {

private static class ToStringHandler implements SpecificHandler {
private final Class<?> annotationType;
private final JavaAnnotation toProxy;
private final JavaAnnotation<?> toProxy;
private final Conversions conversions;

private ToStringHandler(Class<?> annotationType, JavaAnnotation toProxy, Conversions conversions) {
private ToStringHandler(Class<?> annotationType, JavaAnnotation<?> toProxy, Conversions conversions) {
this.annotationType = annotationType;
this.toProxy = toProxy;
this.conversions = conversions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,22 +93,36 @@ static Dependency fromThrowsDeclaration(ThrowsDeclaration<? extends JavaCodeUnit
return createDependencyFromJavaMember(declaration.getLocation(), "throws type", declaration.getRawType());
}

static Dependency fromAnnotation(HasDescription annotatedElement, JavaAnnotation target, JavaClass owner) {
return createDependency(owner, target.getRawType(), annotatedElement, "is annotated with");
static Dependency fromAnnotation(JavaAnnotation<?> target) {
Origin origin = findSuitableOrigin(target);
return createDependency(origin.originClass, origin.originDescription, "is annotated with", target.getRawType());
}

static Dependency fromAnnotationMember(HasDescription annotatedElement, JavaClass memberType, JavaClass owner) {
return createDependency(owner, memberType, annotatedElement, "has annotation member of type");
static Dependency fromAnnotationMember(JavaAnnotation<?> annotation, JavaClass memberType) {
Origin origin = findSuitableOrigin(annotation);
return createDependency(origin.originClass, origin.originDescription, "has annotation member of type", memberType);
}

private static Origin findSuitableOrigin(JavaAnnotation<?> annotation) {
Object annotatedElement = annotation.getAnnotatedElement();
if (annotatedElement instanceof JavaMember) {
JavaMember member = (JavaMember) annotatedElement;
return new Origin(member.getOwner(), member.getDescription());
}
if (annotatedElement instanceof JavaClass) {
JavaClass clazz = (JavaClass) annotatedElement;
return new Origin(clazz, clazz.getDescription());
}
throw new IllegalStateException("Could not find suitable dependency origin for " + annotation);
}

private static Dependency createDependencyFromJavaMember(JavaMember origin, String dependencyType, JavaClass target) {
return createDependency(origin.getOwner(), target, origin, dependencyType);
return createDependency(origin.getOwner(), origin.getDescription(), dependencyType, target);
}

private static Dependency createDependency(
JavaClass originClass, JavaClass targetClass, HasDescription hasOriginDescription, String dependencyType) {
JavaClass originClass, String originDescription, String dependencyType, JavaClass targetClass) {

String originDescription = hasOriginDescription.getDescription();
String targetDescription = bracketFormat(targetClass.getName());
String dependencyDescription = originDescription + " " + dependencyType + " " + targetDescription;
String description = dependencyDescription + " in " + originClass.getSourceCodeLocation();
Expand Down Expand Up @@ -189,6 +203,16 @@ public static JavaClasses toTargetClasses(Iterable<Dependency> dependencies) {
return JavaClasses.of(classes);
}

private static class Origin {
private final JavaClass originClass;
private final String originDescription;

private Origin(JavaClass originClass, String originDescription) {
this.originClass = originClass;
this.originDescription = originDescription;
}
}

public static final class Predicates {
private Predicates() {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import com.google.common.collect.SetMultimap;
import com.tngtech.archunit.Internal;
import com.tngtech.archunit.base.Function;
import com.tngtech.archunit.base.HasDescription;
import com.tngtech.archunit.base.Optional;
import com.tngtech.archunit.core.importer.DomainBuilders;
import com.tngtech.archunit.core.importer.DomainBuilders.ConstructorCallTargetBuilder;
Expand Down Expand Up @@ -78,8 +79,8 @@ public static void completeMembers(JavaClass javaClass, ImportContext importCont
javaClass.completeMembers(importContext);
}

public static JavaAnnotation createJavaAnnotation(JavaAnnotationBuilder builder) {
return new JavaAnnotation(builder);
public static <T extends HasDescription> JavaAnnotation<T> createJavaAnnotation(T owner, JavaAnnotationBuilder builder) {
return new JavaAnnotation<>(owner, builder);
}

public static JavaClassList createJavaClassList(List<JavaClass> elements) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public interface ImportContext {

Optional<JavaStaticInitializer> createStaticInitializer(JavaClass owner);

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

Optional<JavaClass> createEnclosingClass(JavaClass owner);

Expand All @@ -57,9 +57,9 @@ public interface ImportContext {

Set<ThrowsDeclaration<JavaConstructor>> getConstructorThrowsDeclarationsOfType(JavaClass javaClass);

Set<JavaAnnotation> getAnnotationsOfType(JavaClass javaClass);
Set<JavaAnnotation<?>> getAnnotationsOfType(JavaClass javaClass);

Set<JavaAnnotation> getAnnotationsWithParameterOfType(JavaClass javaClass);
Set<JavaAnnotation<?>> getAnnotationsWithParameterOfType(JavaClass javaClass);

Set<JavaMember> getMembersAnnotatedWithType(JavaClass javaClass);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
import java.util.Map;

import com.tngtech.archunit.PublicAPI;
import com.tngtech.archunit.base.HasDescription;
import com.tngtech.archunit.base.Optional;
import com.tngtech.archunit.core.domain.properties.CanBeAnnotated;
import com.tngtech.archunit.core.domain.properties.HasOwner;
import com.tngtech.archunit.core.domain.properties.HasType;
import com.tngtech.archunit.core.importer.DomainBuilders.JavaAnnotationBuilder;
Expand Down Expand Up @@ -65,16 +67,47 @@
* someAnnotation.get("type"); // --&gt; returns JavaClass{String}
* someAnnotation.as(SomeAnnotation.class).type(); // --&gt; returns String.class
* </code></pre>
*
* @param <OWNER> The type of the closest "parent" of this annotation. If this annotation
* is annotated on a class or member, it is that class or member. If this
* annotation is a member of another annotation, it is that annotation.
*/
public final class JavaAnnotation implements HasType, HasOwner<JavaClass> {
public final class JavaAnnotation<OWNER extends HasDescription> implements HasType, HasOwner<OWNER>, HasDescription {
private final JavaClass type;
private final JavaClass owner;
private final OWNER owner;
private final CanBeAnnotated annotatedElement;
private final String description;
private final Map<String, Object> values;

JavaAnnotation(JavaAnnotationBuilder builder) {
JavaAnnotation(OWNER owner, JavaAnnotationBuilder builder) {
this.type = checkNotNull(builder.getType());
this.owner = checkNotNull(builder.getOwner());
this.values = checkNotNull(builder.getValues());
this.owner = checkNotNull(owner);
this.annotatedElement = getAnnotatedElement(owner);
this.description = createDescription();
this.values = checkNotNull(builder.getValues(this));
}

private CanBeAnnotated getAnnotatedElement(Object owner) {
Object candiate = owner;
while (!(candiate instanceof JavaClass) && !(candiate instanceof JavaMember) && (candiate instanceof HasOwner<?>)) {
candiate = ((HasOwner<?>) candiate).getOwner();
}
if (!(candiate instanceof CanBeAnnotated)) {
throw new IllegalArgumentException("Cannot derive annotated element from annotation owner: " + owner);
}
return (CanBeAnnotated) candiate;
}

private String createDescription() {
CanBeAnnotated annotatedElement = getAnnotatedElement();
String descriptionSuffix = annotatedElement instanceof HasDescription
? " on " + startWithLowerCase((HasDescription) annotatedElement)
: "";
return "Annotation <" + type.getName() + ">" + descriptionSuffix;
}

private String startWithLowerCase(HasDescription annotatedElement) {
return annotatedElement.getDescription().substring(0, 1).toLowerCase() + annotatedElement.getDescription().substring(1);
}

/**
Expand All @@ -93,11 +126,40 @@ public JavaClass getRawType() {
return type;
}

/**
* Compare documentation of {@code OWNER} on {@link JavaAnnotation}
*/
@Override
public JavaClass getOwner() {
public OWNER getOwner() {
return owner;
}

/**
* Returns either the element annotated with this {@link JavaAnnotation} (a class or member)
* or in case this annotation is an annotation parameter, the element annotated with an
* annotation that transitively declares this annotation as an annotation parameter.
* <br><br>
* Example:
* <pre><code>
* (1){@literal @}SomeAnnotation class SomeClass {}
* (2) class SomeClass {
* {@literal @}SomeAnnotation SomeField someField;
* }
* (3){@literal @}ComplexAnnotation(param = @SomeAnnotation) class SomeClass {}
* </code></pre>
* For case <code>(1)</code> the result of <code>someAnnotation.getAnnotatedElement()</code>
* would be <code>SomeClass</code>, for case <code>(2)</code>
* the result of <code>someAnnotation.getAnnotatedElement()</code> would be <code>someField</code>
* and for <code>(3)</code> the result of <code>someAnnotation.getAnnotatedElement()</code> would
* also be <code>SomeClass</code>, even though <code>@SomeAnnotation</code> is a parameter of
* <code>@ComplexAnnotation</code>.
* @return The closest element traversing up the tree, that can be annotated
*/
@PublicAPI(usage = ACCESS)
public CanBeAnnotated getAnnotatedElement() {
return annotatedElement;
}

/**
* Returns the value of the property with the given name, i.e. the result of the method with the property name
* of the represented {@link java.lang.annotation.Annotation}. E.g. for
Expand Down Expand Up @@ -137,8 +199,13 @@ public <A extends Annotation> A as(Class<A> annotationType) {
return AnnotationProxy.of(annotationType, this);
}

@Override
public String getDescription() {
return description;
}

@Override
public String toString() {
return getClass().getSimpleName() + '{' + getRawType().getFullName() + '}';
return getClass().getSimpleName() + '{' + type.getName() + '}';
}
}
Loading

0 comments on commit edb98da

Please sign in to comment.