Skip to content

Commit 7e7819d

Browse files
committed
add type parameter dependencies from self
Now that we have the full type parameter info for any `JavaClass` (e.g. `class Foo<T extends Set<? super Bar>>`), we can add type parameter dependencies to `JavaClass.getDirectDependenciesFromSelf()`. In particular any other class that appears within the type signature should count as a dependency of the class, e.g. in the former example `Foo` should report type parameter dependencies on `Set` and `Bar`. I decided to deviate a little bit from the common pattern by making each `JavaTypeVariable` aware of its own dependencies. This way we only need to calculate the dependencies once and can easily reuse them in the next step to register the "toSelf" direction, i.e. all the type parameter dependencies that depend on a specific class. I also did consider to add some sort of visitor API to the type signature, but in the end I went with a simple instanceof chain in this one place. I could not come up with a generic, yet easy and use-/meaningful visitor interface that I would consider a good addition to the public API. Since within ArchUnit there is also only one use case so far, I decided that this part of the domain model will likely be stable enough to not cause any maintainability issues (after all I can't think of any other `JavaType` to be added in the near future and we have the Reflection API to peek into which sorts of `JavaType` came up in the wider context over the last decades). Signed-off-by: Peter Gafert <peter.gafert@tngtech.com>
1 parent 2f4de09 commit 7e7819d

File tree

11 files changed

+302
-37
lines changed

11 files changed

+302
-37
lines changed

archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/service/ServiceHelper.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
package com.tngtech.archunit.example.layers.service;
22

3+
import java.util.Map;
4+
import java.util.Set;
5+
6+
import com.tngtech.archunit.example.layers.controller.SomeUtility;
7+
import com.tngtech.archunit.example.layers.controller.one.SomeEnum;
38
import com.tngtech.archunit.example.layers.security.Secured;
49

510
/**
611
* Well modelled code always has lots of 'helpers' ;-)
712
*/
8-
public class ServiceHelper {
13+
@SuppressWarnings("unused")
14+
public class ServiceHelper<
15+
TYPE_PARAMETER_VIOLATING_LAYER_RULE extends SomeUtility,
16+
ANOTHER_TYPE_PARAMETER_VIOLATING_LAYER_RULE extends Map<?, Set<? super SomeEnum>>> {
17+
918
public Object insecure = new Object();
1019
@Secured
1120
public Object properlySecured = new Object();

archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@
165165
import static com.tngtech.archunit.testutils.ExpectedDependency.field;
166166
import static com.tngtech.archunit.testutils.ExpectedDependency.inheritanceFrom;
167167
import static com.tngtech.archunit.testutils.ExpectedDependency.method;
168+
import static com.tngtech.archunit.testutils.ExpectedDependency.typeParameter;
168169
import static com.tngtech.archunit.testutils.ExpectedLocation.javaClass;
169170
import static com.tngtech.archunit.testutils.ExpectedNaming.simpleNameOf;
170171
import static com.tngtech.archunit.testutils.ExpectedNaming.simpleNameOfAnonymousClassOf;
@@ -738,6 +739,8 @@ Stream<DynamicTest> LayerDependencyRulesTest() {
738739
.by(callFromMethod(ServiceViolatingLayerRules.class, illegalAccessToController)
739740
.toMethod(UseCaseTwoController.class, doSomethingTwo)
740741
.inLine(27).asDependency())
742+
.by(typeParameter(ServiceHelper.class, "TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeUtility.class))
743+
.by(typeParameter(ServiceHelper.class, "ANOTHER_TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeEnum.class))
741744
.by(method(ServiceViolatingLayerRules.class, dependentMethod).withParameter(UseCaseTwoController.class))
742745
.by(method(ServiceViolatingLayerRules.class, dependentMethod).withReturnType(SomeGuiController.class))
743746
.by(annotatedClass(ServiceViolatingLayerRules.class).withAnnotationParameterType(ComplexControllerAnnotation.class))
@@ -790,16 +793,13 @@ Stream<DynamicTest> LayerDependencyRulesTest() {
790793
.by(callFromMethod(ServiceViolatingLayerRules.class, illegalAccessToController)
791794
.toMethod(UseCaseTwoController.class, doSomethingTwo)
792795
.inLine(27).asDependency())
793-
.by(method(ServiceViolatingLayerRules.class, dependentMethod)
794-
.withParameter(UseCaseTwoController.class))
795-
.by(method(ServiceViolatingLayerRules.class, dependentMethod)
796-
.withReturnType(SomeGuiController.class))
797-
.by(field(ServiceHelper.class, "properlySecured")
798-
.withAnnotationType(Secured.class))
799-
.by(method(ServiceViolatingLayerRules.class, "properlySecured")
800-
.withAnnotationType(Secured.class))
801-
.by(constructor(ServiceHelper.class)
802-
.withAnnotationType(Secured.class))
796+
.by(typeParameter(ServiceHelper.class, "TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeUtility.class))
797+
.by(typeParameter(ServiceHelper.class, "ANOTHER_TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeEnum.class))
798+
.by(method(ServiceViolatingLayerRules.class, dependentMethod).withParameter(UseCaseTwoController.class))
799+
.by(method(ServiceViolatingLayerRules.class, dependentMethod).withReturnType(SomeGuiController.class))
800+
.by(field(ServiceHelper.class, "properlySecured").withAnnotationType(Secured.class))
801+
.by(method(ServiceViolatingLayerRules.class, "properlySecured").withAnnotationType(Secured.class))
802+
.by(constructor(ServiceHelper.class).withAnnotationType(Secured.class))
803803
.by(annotatedClass(ServiceViolatingDaoRules.class).annotatedWith(MyService.class))
804804
.by(annotatedClass(ServiceViolatingLayerRules.class).annotatedWith(MyService.class))
805805
.by(annotatedClass(ServiceImplementation.class).annotatedWith(MyService.class))

archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedDependency.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ public static InheritanceCreator inheritanceFrom(Class<?> clazz) {
2525
return new InheritanceCreator(clazz);
2626
}
2727

28+
public static TypeParameterCreator typeParameter(Class<?> clazz, String typeParameterName) {
29+
return new TypeParameterCreator(clazz, typeParameterName);
30+
}
31+
2832
public static AnnotationDependencyCreator annotatedClass(Class<?> clazz) {
2933
return new AnnotationDependencyCreator(clazz);
3034
}
@@ -85,6 +89,21 @@ public ExpectedDependency implementing(Class<?> anInterface) {
8589
}
8690
}
8791

92+
public static class TypeParameterCreator {
93+
private final Class<?> clazz;
94+
private final String typeParameterName;
95+
96+
private TypeParameterCreator(Class<?> clazz, String typeParameterName) {
97+
this.clazz = clazz;
98+
this.typeParameterName = typeParameterName;
99+
}
100+
101+
public ExpectedDependency dependingOn(Class<?> typeParameterDependency) {
102+
return new ExpectedDependency(clazz, typeParameterDependency,
103+
getDependencyPattern(clazz.getName(), "has type parameter '" + typeParameterName + "' depending on", typeParameterDependency.getName(), 0));
104+
}
105+
}
106+
88107
public static class AccessCreator {
89108
private final Class<?> originClass;
90109

archunit/src/main/java/com/tngtech/archunit/core/domain/Dependency.java

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -108,26 +108,31 @@ static Optional<Dependency> tryCreateFromInstanceofCheck(InstanceofCheck instanc
108108
}
109109

110110
static Optional<Dependency> tryCreateFromAnnotation(JavaAnnotation<?> target) {
111-
Origin origin = findSuitableOrigin(target);
111+
Origin origin = findSuitableOrigin(target, target.getAnnotatedElement());
112112
return tryCreateDependency(origin.originClass, origin.originDescription, "is annotated with", target.getRawType());
113113
}
114114

115115
static Optional<Dependency> tryCreateFromAnnotationMember(JavaAnnotation<?> annotation, JavaClass memberType) {
116-
Origin origin = findSuitableOrigin(annotation);
116+
Origin origin = findSuitableOrigin(annotation, annotation.getAnnotatedElement());
117117
return tryCreateDependency(origin.originClass, origin.originDescription, "has annotation member of type", memberType);
118118
}
119119

120-
private static Origin findSuitableOrigin(JavaAnnotation<?> annotation) {
121-
Object annotatedElement = annotation.getAnnotatedElement();
122-
if (annotatedElement instanceof JavaMember) {
123-
JavaMember member = (JavaMember) annotatedElement;
120+
static Optional<Dependency> tryCreateFromTypeParameter(JavaTypeVariable<?> typeParameter, JavaClass typeParameterDependency) {
121+
String dependencyType = "has type parameter '" + typeParameter.getName() + "' depending on";
122+
Origin origin = findSuitableOrigin(typeParameter, typeParameter.getOwner());
123+
return tryCreateDependency(origin.originClass, origin.originDescription, dependencyType, typeParameterDependency);
124+
}
125+
126+
private static Origin findSuitableOrigin(Object dependencyCause, Object originCandidate) {
127+
if (originCandidate instanceof JavaMember) {
128+
JavaMember member = (JavaMember) originCandidate;
124129
return new Origin(member.getOwner(), member.getDescription());
125130
}
126-
if (annotatedElement instanceof JavaClass) {
127-
JavaClass clazz = (JavaClass) annotatedElement;
131+
if (originCandidate instanceof JavaClass) {
132+
JavaClass clazz = (JavaClass) originCandidate;
128133
return new Origin(clazz, clazz.getDescription());
129134
}
130-
throw new IllegalStateException("Could not find suitable dependency origin for " + annotation);
135+
throw new IllegalStateException("Could not find suitable dependency origin for " + dependencyCause);
131136
}
132137

133138
private static Optional<Dependency> tryCreateDependencyFromJavaMember(JavaMember origin, String dependencyType, JavaClass target) {

archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@ public Optional<JavaAnnotation<JavaClass>> tryGetAnnotationOfType(String typeNam
575575
}
576576

577577
@PublicAPI(usage = ACCESS)
578-
public List<JavaTypeVariable<JavaClass>> getTypeParameters() {
578+
public JavaTypeParameters<JavaClass> getTypeParameters() {
579579
return typeParameters;
580580
}
581581

archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDependencies.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ public Set<Dependency> get() {
7171
result.addAll(constructorParameterDependenciesFromSelf());
7272
result.addAll(annotationDependenciesFromSelf());
7373
result.addAll(instanceofCheckDependenciesFromSelf());
74+
result.addAll(typeParameterDependenciesFromSelf());
7475
return result.build();
7576
}
7677
});
@@ -220,6 +221,10 @@ private Set<Dependency> instanceofCheckDependenciesFromSelf() {
220221
return result.build();
221222
}
222223

224+
private Set<Dependency> typeParameterDependenciesFromSelf() {
225+
return javaClass.getTypeParameters().getDependenciesFromSelf();
226+
}
227+
223228
private <T extends HasDescription & HasAnnotations<?>> Set<Dependency> annotationDependencies(Set<T> annotatedObjects) {
224229
ImmutableSet.Builder<Dependency> result = ImmutableSet.builder();
225230
for (T annotated : annotatedObjects) {

archunit/src/main/java/com/tngtech/archunit/core/domain/JavaTypeParameters.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
package com.tngtech.archunit.core.domain;
1717

1818
import java.util.List;
19+
import java.util.Set;
1920

2021
import com.google.common.collect.ImmutableList;
22+
import com.google.common.collect.ImmutableSet;
2123
import com.tngtech.archunit.PublicAPI;
2224
import com.tngtech.archunit.base.ForwardingList;
2325
import com.tngtech.archunit.base.HasDescription;
@@ -40,4 +42,12 @@ public final class JavaTypeParameters<OWNER extends HasDescription> extends Forw
4042
protected List<JavaTypeVariable<OWNER>> delegate() {
4143
return typeParameters;
4244
}
45+
46+
Set<Dependency> getDependenciesFromSelf() {
47+
ImmutableSet.Builder<Dependency> result = ImmutableSet.builder();
48+
for (JavaTypeVariable<?> typeVariable : typeParameters) {
49+
result.addAll(typeVariable.getDependenciesFromSelf());
50+
}
51+
return result.build();
52+
}
4353
}

archunit/src/main/java/com/tngtech/archunit/core/domain/JavaTypeVariable.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,23 @@
1717

1818
import java.lang.reflect.TypeVariable;
1919
import java.util.List;
20+
import java.util.Set;
2021

2122
import com.google.common.base.Joiner;
2223
import com.google.common.collect.FluentIterable;
24+
import com.google.common.collect.ImmutableSet;
2325
import com.tngtech.archunit.PublicAPI;
2426
import com.tngtech.archunit.base.HasDescription;
2527
import com.tngtech.archunit.core.domain.properties.HasOwner;
2628
import com.tngtech.archunit.core.domain.properties.HasUpperBounds;
2729

30+
import static com.google.common.collect.Iterables.concat;
2831
import static com.google.common.collect.Iterables.getOnlyElement;
2932
import static com.tngtech.archunit.PublicAPI.Usage.ACCESS;
3033
import static com.tngtech.archunit.base.Guava.toGuava;
3134
import static com.tngtech.archunit.core.domain.properties.HasName.Functions.GET_NAME;
3235
import static java.util.Collections.emptyList;
36+
import static java.util.Collections.emptySet;
3337

3438
/**
3539
* Represents a type variable used by generic types and members.<br>
@@ -49,6 +53,7 @@ public final class JavaTypeVariable<OWNER extends HasDescription> implements Jav
4953
private final OWNER owner;
5054
private List<JavaType> upperBounds = emptyList();
5155
private JavaClass erasure;
56+
private Set<Dependency> dependencies = emptySet();
5257

5358
JavaTypeVariable(String name, OWNER owner, JavaClass erasure) {
5459
this.name = name;
@@ -59,6 +64,42 @@ public final class JavaTypeVariable<OWNER extends HasDescription> implements Jav
5964
void setUpperBounds(List<JavaType> upperBounds) {
6065
this.upperBounds = upperBounds;
6166
erasure = upperBounds.isEmpty() ? erasure : upperBounds.get(0).toErasure();
67+
ImmutableSet.Builder<Dependency> dependenciesBuilder = ImmutableSet.builder();
68+
for (JavaType bound : getUpperBounds()) {
69+
for (JavaClass typeParameterDependency : dependenciesOfType(bound)) {
70+
dependenciesBuilder.addAll(Dependency.tryCreateFromTypeParameter(this, typeParameterDependency).asSet());
71+
}
72+
}
73+
dependencies = dependenciesBuilder.build();
74+
}
75+
76+
private Iterable<JavaClass> dependenciesOfType(JavaType javaType) {
77+
ImmutableSet.Builder<JavaClass> result = ImmutableSet.builder();
78+
if (javaType instanceof JavaClass) {
79+
result.add((JavaClass) javaType);
80+
} else if (javaType instanceof JavaParameterizedType) {
81+
result.addAll(dependenciesOfParameterizedType((JavaParameterizedType) javaType));
82+
} else if (javaType instanceof JavaWildcardType) {
83+
result.addAll(dependenciesOfWildcardType((JavaWildcardType) javaType));
84+
}
85+
return result.build();
86+
}
87+
88+
private Set<JavaClass> dependenciesOfParameterizedType(JavaParameterizedType parameterizedType) {
89+
ImmutableSet.Builder<JavaClass> result = ImmutableSet.<JavaClass>builder()
90+
.add(parameterizedType.toErasure());
91+
for (JavaType typeArgument : parameterizedType.getActualTypeArguments()) {
92+
result.addAll(dependenciesOfType(typeArgument));
93+
}
94+
return result.build();
95+
}
96+
97+
private Set<JavaClass> dependenciesOfWildcardType(JavaWildcardType javaType) {
98+
ImmutableSet.Builder<JavaClass> result = ImmutableSet.builder();
99+
for (JavaType bound : concat(javaType.getUpperBounds(), javaType.getLowerBounds())) {
100+
result.addAll(dependenciesOfType(bound));
101+
}
102+
return result.build();
62103
}
63104

64105
/**
@@ -116,6 +157,10 @@ public JavaClass toErasure() {
116157
return erasure;
117158
}
118159

160+
Set<Dependency> getDependenciesFromSelf() {
161+
return dependencies;
162+
}
163+
119164
@Override
120165
public String toString() {
121166
String bounds = printExtendsClause() ? " extends " + joinTypeNames(upperBounds) : "";

archunit/src/test/java/com/tngtech/archunit/core/domain/DependencyTest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,20 @@ public void Dependency_from_member_annotation_member(JavaMember annotatedMember)
173173
.contains(annotatedMember.getDescription() + " has annotation member of type <" + memberType.getName() + ">");
174174
}
175175

176+
@Test
177+
public void Dependency_from_type_parameter() {
178+
JavaClass javaClass = importClassesWithContext(ClassWithTypeParameters.class, String.class).get(ClassWithTypeParameters.class);
179+
JavaTypeVariable<?> typeParameter = javaClass.getTypeParameters().get(0);
180+
181+
Dependency dependency = Dependency.tryCreateFromTypeParameter(typeParameter, typeParameter.getUpperBounds().get(0).toErasure()).get();
182+
183+
assertThatType(dependency.getOriginClass()).matches(ClassWithTypeParameters.class);
184+
assertThatType(dependency.getTargetClass()).matches(String.class);
185+
assertThat(dependency.getDescription()).as("description").contains(String.format(
186+
"Class <%s> has type parameter '%s' depending on <%s> in (%s.java:0)",
187+
ClassWithTypeParameters.class.getName(), typeParameter.getName(), String.class.getName(), getClass().getSimpleName()));
188+
}
189+
176190
@Test
177191
public void origin_predicates_match() {
178192
assertThatDependency(Origin.class, Target.class)
@@ -307,4 +321,8 @@ public ClassWithAnnotatedMembers(Object annotatedField) {
307321
void annotatedMethod() {
308322
}
309323
}
324+
325+
@SuppressWarnings("unused")
326+
private static class ClassWithTypeParameters<T extends String> {
327+
}
310328
}

0 commit comments

Comments
 (0)