From a6ed890cc00e83752b2a898d8122adc2c9cfa272 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Thu, 16 May 2024 12:34:28 +0200 Subject: [PATCH] GH-598 - Invalid dependency into named interface now gets reported properly. --- .../modulith/core/ApplicationModule.java | 20 +++++++++++++++++-- .../modulith/core/NamedInterface.java | 11 ++++++++++ .../modulith/core/NamedInterfaces.java | 20 +++++++++++++++++++ .../core/NamedInterfacesUnitTests.java | 11 ++++++++++ 4 files changed, 60 insertions(+), 2 deletions(-) diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java index 7ded1e4c..60bd60d1 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java @@ -579,6 +579,10 @@ public class ApplicationModule { .collect(Classes.toClasses()); } + private String getQualifiedName(NamedInterface namedInterface) { + return namedInterface.getQualifiedName(getName()); + } + private static Classes filterSpringBeans(JavaPackage source) { Map> collect = source.that(isConfiguration()).stream() // @@ -971,8 +975,20 @@ public class ApplicationModule { // Check explicitly defined allowed targets if (!declaredDependencies.isAllowedDependency(target)) { - var message = "Module '%s' depends on module '%s' via %s -> %s. Allowed targets: %s." // - .formatted(originModule.getName(), targetModule.getName(), source.getName(), target.getName(), + var targetNamedInterfaces = targetModule.getNamedInterfaces() + .getNamedInterfacesContaining(target) + .filter(NamedInterface::isNamed) + .toList(); + + var targetString = targetNamedInterfaces.isEmpty() + ? "module '%s'".formatted(targetModule.getName()) + : "named interface(s) '%s'".formatted( + targetNamedInterfaces.stream() + .map(targetModule::getQualifiedName) + .collect(Collectors.joining(", "))); + + var message = "Module '%s' depends on %s via %s -> %s. Allowed targets: %s." // + .formatted(originModule.getName(), targetString, source.getName(), target.getName(), declaredDependencies.toString()); return violations.and(new Violation(message)); diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterface.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterface.java index d82ba013..05b3b7f3 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterface.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterface.java @@ -125,6 +125,10 @@ public class NamedInterface implements Iterable { return name; } + String getQualifiedName(String qualifier) { + return qualifier + " :: " + name; + } + /** * Returns whether this is the unnamed (implicit) {@link NamedInterface}. */ @@ -132,6 +136,13 @@ public class NamedInterface implements Iterable { return name.equals(UNNAMED_NAME); } + /** + * Return whether this {@link NamedInterface} has an explicit name. + */ + public boolean isNamed() { + return !isUnnamed(); + } + /** * Returns whether the {@link NamedInterface} contains the given {@link JavaClass}. * diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterfaces.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterfaces.java index 42a8d2d7..27761539 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterfaces.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterfaces.java @@ -138,6 +138,20 @@ public class NamedInterfaces implements Iterable { .orElseThrow(() -> new IllegalStateException("No unnamed interface found!")); } + /** + * Returns all named interfaces that contain the given type. + * + * @param type must not be {@literal null}. + * @return will never be {@literal null}. + */ + public Stream getNamedInterfacesContaining(JavaClass type) { + + Assert.notNull(type, "Type must not be null!"); + + return namedInterfaces.stream() + .filter(it -> it.contains(type)); + } + /* * (non-Javadoc) * @see java.lang.Iterable#iterator() @@ -183,6 +197,12 @@ public class NamedInterfaces implements Iterable { return new NamedInterfaces(namedInterfaces); } + Stream getNamedInterfacesContaining(Class type) { + + return namedInterfaces.stream() + .filter(it -> it.contains(type)); + } + private static NamedInterfaces of(NamedInterface interfaces) { return new NamedInterfaces(List.of(interfaces)); } diff --git a/spring-modulith-core/src/test/java/org/springframework/modulith/core/NamedInterfacesUnitTests.java b/spring-modulith-core/src/test/java/org/springframework/modulith/core/NamedInterfacesUnitTests.java index 24438d8e..dabb9ea6 100644 --- a/spring-modulith-core/src/test/java/org/springframework/modulith/core/NamedInterfacesUnitTests.java +++ b/spring-modulith-core/src/test/java/org/springframework/modulith/core/NamedInterfacesUnitTests.java @@ -64,6 +64,17 @@ class NamedInterfacesUnitTests { .withMessageContaining(InvalidDefaultNamedInterface.class.getSimpleName()); } + @Test // GH-595 + void detectsNamedInterfacesATypeIsContainedIn() { + + var javaPackage = TestUtils.getPackage(RootType.class); + var interfaces = NamedInterfaces.discoverNamedInterfaces(javaPackage); + + assertThat(interfaces.getNamedInterfacesContaining(AdditionalSpiType.class)) + .extracting(NamedInterface::getName) + .containsExactlyInAnyOrder("spi", "kpi"); + } + private static void assertInterfaceContains(NamedInterfaces interfaces, String name, Class... types) { var classNames = Arrays.stream(types).map(Class::getName).toArray(String[]::new);