From 3855479c7c02d07fedd1f9a7c954a1e01f39829d Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Thu, 16 May 2024 12:34:28 +0200 Subject: [PATCH] GH-595 - 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 c104efa4..ceafa3bd 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 @@ -599,6 +599,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() // @@ -991,8 +995,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 08f31449..69ecff1c 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 @@ -132,6 +132,10 @@ public class NamedInterface implements Iterable { return name; } + String getQualifiedName(String qualifier) { + return qualifier + " :: " + name; + } + /** * Returns whether this is the unnamed (implicit) {@link NamedInterface}. */ @@ -139,6 +143,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 bd43ef6c..5dbc4dea 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 @@ -153,6 +153,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() @@ -198,6 +212,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 9505971b..853a630a 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 @@ -79,6 +79,17 @@ class NamedInterfacesUnitTests { assertInterfaceContains(interfaces, NamedInterface.UNNAMED_NAME, RootType.class, Internal.class); } + @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);