diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/service/ServiceHelper.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/service/ServiceHelper.java index 1210abe8a4..f72fb50ba5 100644 --- a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/service/ServiceHelper.java +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/service/ServiceHelper.java @@ -1,11 +1,20 @@ package com.tngtech.archunit.example.layers.service; +import java.util.Map; +import java.util.Set; + +import com.tngtech.archunit.example.layers.controller.SomeUtility; +import com.tngtech.archunit.example.layers.controller.one.SomeEnum; import com.tngtech.archunit.example.layers.security.Secured; /** * Well modelled code always has lots of 'helpers' ;-) */ -public class ServiceHelper { +@SuppressWarnings("unused") +public class ServiceHelper< + TYPE_PARAMETER_VIOLATING_LAYER_RULE extends SomeUtility, + ANOTHER_TYPE_PARAMETER_VIOLATING_LAYER_RULE extends Map>> { + public Object insecure = new Object(); @Secured public Object properlySecured = new Object(); diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java index 907d20b6bb..f0727d8c7d 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java @@ -166,6 +166,7 @@ import static com.tngtech.archunit.testutils.ExpectedDependency.field; import static com.tngtech.archunit.testutils.ExpectedDependency.inheritanceFrom; import static com.tngtech.archunit.testutils.ExpectedDependency.method; +import static com.tngtech.archunit.testutils.ExpectedDependency.typeParameter; import static com.tngtech.archunit.testutils.ExpectedLocation.javaClass; import static com.tngtech.archunit.testutils.ExpectedNaming.simpleNameOf; import static com.tngtech.archunit.testutils.ExpectedNaming.simpleNameOfAnonymousClassOf; @@ -739,6 +740,8 @@ Stream LayerDependencyRulesTest() { .by(callFromMethod(ServiceViolatingLayerRules.class, illegalAccessToController) .toMethod(UseCaseTwoController.class, doSomethingTwo) .inLine(25).asDependency()) + .by(typeParameter(ServiceHelper.class, "TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeUtility.class)) + .by(typeParameter(ServiceHelper.class, "ANOTHER_TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeEnum.class)) .by(method(ServiceViolatingLayerRules.class, dependentMethod).withParameter(UseCaseTwoController.class)) .by(method(ServiceViolatingLayerRules.class, dependentMethod).withReturnType(SomeGuiController.class)) .by(method(ServiceViolatingLayerRules.class, dependentOnComponentTypeMethod).withParameter(UseCaseTwoController[].class)) @@ -796,6 +799,8 @@ Stream LayerDependencyRulesTest() { .by(callFromMethod(ServiceViolatingLayerRules.class, illegalAccessToController) .toMethod(UseCaseTwoController.class, doSomethingTwo) .inLine(25).asDependency()) + .by(typeParameter(ServiceHelper.class, "TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeUtility.class)) + .by(typeParameter(ServiceHelper.class, "ANOTHER_TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeEnum.class)) .by(method(ServiceViolatingLayerRules.class, dependentMethod).withParameter(UseCaseTwoController.class)) .by(method(ServiceViolatingLayerRules.class, dependentMethod).withReturnType(SomeGuiController.class)) .by(method(ServiceViolatingLayerRules.class, dependentOnComponentTypeMethod).withParameter(UseCaseTwoController[].class)) @@ -863,6 +868,8 @@ Stream LayeredArchitectureTest() { .inLine(27) .asDependency()) + .by(typeParameter(ServiceHelper.class, "TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeUtility.class)) + .by(typeParameter(ServiceHelper.class, "ANOTHER_TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeEnum.class)) .by(method(ServiceViolatingLayerRules.class, dependentMethod).withParameter(UseCaseTwoController.class)) .by(method(ServiceViolatingLayerRules.class, dependentMethod).withReturnType(SomeGuiController.class)) .by(method(ServiceViolatingLayerRules.class, dependentOnComponentTypeMethod) diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedDependency.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedDependency.java index 96d42baa74..7a19fce21d 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedDependency.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedDependency.java @@ -25,6 +25,10 @@ public static InheritanceCreator inheritanceFrom(Class clazz) { return new InheritanceCreator(clazz); } + public static TypeParameterCreator typeParameter(Class clazz, String typeParameterName) { + return new TypeParameterCreator(clazz, typeParameterName); + } + public static AnnotationDependencyCreator annotatedClass(Class clazz) { return new AnnotationDependencyCreator(clazz); } @@ -85,6 +89,21 @@ public ExpectedDependency implementing(Class anInterface) { } } + public static class TypeParameterCreator { + private final Class clazz; + private final String typeParameterName; + + private TypeParameterCreator(Class clazz, String typeParameterName) { + this.clazz = clazz; + this.typeParameterName = typeParameterName; + } + + public ExpectedDependency dependingOn(Class typeParameterDependency) { + return new ExpectedDependency(clazz, typeParameterDependency, + getDependencyPattern(clazz.getName(), "has type parameter '" + typeParameterName + "' depending on", typeParameterDependency.getName(), 0)); + } + } + public static class AccessCreator { private final Class originClass; diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/Dependency.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/Dependency.java index 950f2c55ed..e3de3ea06e 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/Dependency.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/Dependency.java @@ -120,26 +120,31 @@ static Set tryCreateFromInstanceofCheck(InstanceofCheck instanceofCh } static Set tryCreateFromAnnotation(JavaAnnotation target) { - Origin origin = findSuitableOrigin(target); + Origin origin = findSuitableOrigin(target, target.getAnnotatedElement()); return tryCreateDependency(origin.originClass, origin.originDescription, "is annotated with", target.getRawType()); } static Set tryCreateFromAnnotationMember(JavaAnnotation annotation, JavaClass memberType) { - Origin origin = findSuitableOrigin(annotation); + Origin origin = findSuitableOrigin(annotation, annotation.getAnnotatedElement()); return tryCreateDependency(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; + static Set tryCreateFromTypeParameter(JavaTypeVariable typeParameter, JavaClass typeParameterDependency) { + String dependencyType = "has type parameter '" + typeParameter.getName() + "' depending on"; + Origin origin = findSuitableOrigin(typeParameter, typeParameter.getOwner()); + return tryCreateDependency(origin.originClass, origin.originDescription, dependencyType, typeParameterDependency); + } + + private static Origin findSuitableOrigin(Object dependencyCause, Object originCandidate) { + if (originCandidate instanceof JavaMember) { + JavaMember member = (JavaMember) originCandidate; return new Origin(member.getOwner(), member.getDescription()); } - if (annotatedElement instanceof JavaClass) { - JavaClass clazz = (JavaClass) annotatedElement; + if (originCandidate instanceof JavaClass) { + JavaClass clazz = (JavaClass) originCandidate; return new Origin(clazz, clazz.getDescription()); } - throw new IllegalStateException("Could not find suitable dependency origin for " + annotation); + throw new IllegalStateException("Could not find suitable dependency origin for " + dependencyCause); } private static Set tryCreateDependencyFromJavaMember(JavaMember origin, String dependencyType, JavaClass target) { 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 902dd31f6a..8038cd3b76 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 @@ -25,6 +25,7 @@ import com.tngtech.archunit.core.domain.properties.HasAnnotations; import static com.google.common.base.Suppliers.memoize; +import static com.google.common.collect.Iterables.concat; class JavaClassDependencies { private final JavaClass javaClass; @@ -49,6 +50,7 @@ public Set get() { result.addAll(constructorParameterDependenciesFromSelf()); result.addAll(annotationDependenciesFromSelf()); result.addAll(instanceofCheckDependenciesFromSelf()); + result.addAll(typeParameterDependenciesFromSelf()); return result.build(); } }); @@ -139,6 +141,53 @@ private Set instanceofCheckDependenciesFromSelf() { return result.build(); } + private Set typeParameterDependenciesFromSelf() { + ImmutableSet.Builder result = ImmutableSet.builder(); + for (JavaTypeVariable typeVariable : javaClass.getTypeParameters()) { + result.addAll(getDependenciesFromTypeParameter(typeVariable)); + } + return result.build(); + } + + private Set getDependenciesFromTypeParameter(JavaTypeVariable typeVariable) { + ImmutableSet.Builder dependenciesBuilder = ImmutableSet.builder(); + for (JavaType bound : typeVariable.getUpperBounds()) { + for (JavaClass typeParameterDependency : dependenciesOfType(bound)) { + dependenciesBuilder.addAll(Dependency.tryCreateFromTypeParameter(typeVariable, typeParameterDependency)); + } + } + return dependenciesBuilder.build(); + } + + private static Iterable dependenciesOfType(JavaType javaType) { + ImmutableSet.Builder result = ImmutableSet.builder(); + if (javaType instanceof JavaClass) { + result.add((JavaClass) javaType); + } else if (javaType instanceof JavaParameterizedType) { + result.addAll(dependenciesOfParameterizedType((JavaParameterizedType) javaType)); + } else if (javaType instanceof JavaWildcardType) { + result.addAll(dependenciesOfWildcardType((JavaWildcardType) javaType)); + } + return result.build(); + } + + private static Set dependenciesOfParameterizedType(JavaParameterizedType parameterizedType) { + ImmutableSet.Builder result = ImmutableSet.builder() + .add(parameterizedType.toErasure()); + for (JavaType typeArgument : parameterizedType.getActualTypeArguments()) { + result.addAll(dependenciesOfType(typeArgument)); + } + return result.build(); + } + + private static Set dependenciesOfWildcardType(JavaWildcardType javaType) { + ImmutableSet.Builder result = ImmutableSet.builder(); + for (JavaType bound : concat(javaType.getUpperBounds(), javaType.getLowerBounds())) { + result.addAll(dependenciesOfType(bound)); + } + return result.build(); + } + private > Set annotationDependencies(Set annotatedObjects) { ImmutableSet.Builder result = ImmutableSet.builder(); for (T annotated : annotatedObjects) { diff --git a/archunit/src/test/java/com/tngtech/archunit/core/domain/DependencyTest.java b/archunit/src/test/java/com/tngtech/archunit/core/domain/DependencyTest.java index 0dd11a0225..112b1c29c5 100644 --- a/archunit/src/test/java/com/tngtech/archunit/core/domain/DependencyTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/domain/DependencyTest.java @@ -257,6 +257,24 @@ public void Dependency_from_member_annotation_member(JavaMember annotatedMember) .contains(annotatedMember.getDescription() + " has annotation member of type <" + memberType.getName() + ">"); } + @Test + public void Dependency_from_type_parameter() { + @SuppressWarnings("unused") + class ClassWithTypeParameters { + } + + JavaClass javaClass = importClassesWithContext(ClassWithTypeParameters.class, String.class).get(ClassWithTypeParameters.class); + JavaTypeVariable typeParameter = javaClass.getTypeParameters().get(0); + + Dependency dependency = getOnlyElement(Dependency.tryCreateFromTypeParameter(typeParameter, typeParameter.getUpperBounds().get(0).toErasure())); + + assertThatType(dependency.getOriginClass()).matches(ClassWithTypeParameters.class); + assertThatType(dependency.getTargetClass()).matches(String.class); + assertThat(dependency.getDescription()).as("description").contains(String.format( + "Class <%s> has type parameter '%s' depending on <%s> in (%s.java:0)", + ClassWithTypeParameters.class.getName(), typeParameter.getName(), String.class.getName(), getClass().getSimpleName())); + } + @Test public void origin_predicates_match() { assertThatDependency(Origin.class, Target.class) 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 c23d5e9218..bfb0c160df 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 @@ -1,5 +1,7 @@ package com.tngtech.archunit.core.domain; +import java.io.BufferedInputStream; +import java.io.File; import java.io.Serializable; import java.lang.annotation.Retention; import java.util.AbstractList; @@ -7,6 +9,7 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import com.google.common.collect.FluentIterable; @@ -574,6 +577,59 @@ public void finds_array_component_types_as_dependencies_from_self() { .inLocation(ArrayComponentTypeDependencies.class, 18)); } + @Test + public void direct_dependencies_from_self_by_type_parameter() { + @SuppressWarnings("unused") + class ClassWithTypeParameters< + FIRST extends List & Serializable & Comparable, + SECOND extends Map< + Map.Entry>, + Map>>>>>>>, + SELF extends ClassWithTypeParameters> { + } + + JavaClass javaClass = importClasses(ClassWithTypeParameters.class).get(ClassWithTypeParameters.class); + + assertThatDependencies(javaClass.getDirectDependenciesFromSelf()) + .contain(from(ClassWithTypeParameters.class).to(List.class).inLocation(getClass(), 0) + .withDescriptionContaining("type parameter 'FIRST' depending on") + + .from(ClassWithTypeParameters.class).to(Serializable.class).inLocation(getClass(), 0) + .withDescriptionContaining("type parameter 'FIRST' depending on") + + .from(ClassWithTypeParameters.class).to(Comparable.class).inLocation(getClass(), 0) + .withDescriptionContaining("type parameter 'FIRST' depending on") + + .from(ClassWithTypeParameters.class).to(Map.class).inLocation(getClass(), 0) + .withDescriptionContaining("type parameter 'SECOND' depending on") + + .from(ClassWithTypeParameters.class).to(Map.Entry.class).inLocation(getClass(), 0) + .withDescriptionContaining("type parameter 'SECOND' depending on") + + .from(ClassWithTypeParameters.class).to(String.class).inLocation(getClass(), 0) + .withDescriptionContaining("type parameter 'SECOND' depending on") + + .from(ClassWithTypeParameters.class).to(BufferedInputStream[][].class).inLocation(getClass(), 0) + .withDescriptionContaining("type parameter 'SECOND' depending on") + + .from(ClassWithTypeParameters.class).to(Serializable.class).inLocation(getClass(), 0) + .withDescriptionContaining("type parameter 'SECOND' depending on") + + .from(ClassWithTypeParameters.class).to(List.class).inLocation(getClass(), 0) + .withDescriptionContaining("type parameter 'SECOND' depending on") + + .from(ClassWithTypeParameters.class).to(Set.class).inLocation(getClass(), 0) + .withDescriptionContaining("type parameter 'SECOND' depending on") + + .from(ClassWithTypeParameters.class).to(Iterable.class).inLocation(getClass(), 0) + .withDescriptionContaining("type parameter 'SECOND' depending on") + + .from(ClassWithTypeParameters.class).to(File.class).inLocation(getClass(), 0) + .withDescriptionContaining("type parameter 'SECOND' depending on") + ); + } + @Test public void direct_dependencies_from_self_finds_correct_set_of_target_types() { JavaClass javaClass = importPackagesOf(getClass()).get(ClassWithAnnotationDependencies.class); @@ -743,6 +799,34 @@ public void finds_array_component_types_as_dependencies_to_self() { .inLocation(ArrayComponentTypeDependencies.class, 18)); } + @Test + public void direct_dependencies_to_self_by_type_parameter() { + class ClassOtherTypeSignaturesDependOn { + } + @SuppressWarnings("unused") + class FirstDependingOnOtherThroughTypeParameter { + } + @SuppressWarnings("unused") + class SecondDependingOnOtherThroughTypeParameter< + U extends Map>>, + V extends Map> { + } + + JavaClass someClass = importClasses(ClassOtherTypeSignaturesDependOn.class, FirstDependingOnOtherThroughTypeParameter.class, SecondDependingOnOtherThroughTypeParameter.class) + .get(ClassOtherTypeSignaturesDependOn.class); + + assertThatDependencies(someClass.getDirectDependenciesToSelf()) + .contain(from(FirstDependingOnOtherThroughTypeParameter.class).to(ClassOtherTypeSignaturesDependOn.class).inLocation(getClass(), 0) + .withDescriptionContaining("type parameter 'T' depending on") + + .from(SecondDependingOnOtherThroughTypeParameter.class).to(ClassOtherTypeSignaturesDependOn.class).inLocation(getClass(), 0) + .withDescriptionContaining("type parameter 'U' depending on") + + .from(SecondDependingOnOtherThroughTypeParameter.class).to(ClassOtherTypeSignaturesDependOn.class).inLocation(getClass(), 0) + .withDescriptionContaining("type parameter 'V' depending on") + ); + } + @Test public void direct_dependencies_to_self_finds_correct_set_of_origin_types() { JavaClasses classes = importPackagesOf(getClass());