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 1e72fa4b..74ec2fd0 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 @@ -1201,18 +1201,17 @@ public class ApplicationModule implements Comparable { // Parent child relationships - return targetModule.getParentModule(modules) - .filter(it -> !it.equals(originModule)) - .map(__ -> { + if (!haveSameParentOrDirectParentRelationship(originModule, targetModule, modules)) { - var violationText = INVALID_SUB_MODULE_REFERENCE - .formatted(originModule.getName(), targetModule.getName(), - FormatableType.of(source).getAbbreviatedFullName(originModule), - FormatableType.of(target).getAbbreviatedFullName(targetModule)); + var violationText = INVALID_SUB_MODULE_REFERENCE + .formatted(originModule.getName(), targetModule.getName(), + FormatableType.of(source).getAbbreviatedFullName(originModule), + FormatableType.of(target).getAbbreviatedFullName(targetModule)); - return violations.and(new Violation(violationText)); - }) - .orElse(violations); + return violations.and(new Violation(violationText)); + } + + return violations; } ApplicationModule getExistingModuleOf(JavaClass javaClass, ApplicationModules modules) { @@ -1347,6 +1346,27 @@ public class ApplicationModule implements Comparable { private static boolean isInjectionPoint(JavaMember unit) { return INJECTION_TYPES.stream().anyMatch(type -> unit.isAnnotatedWith(type)); } + + private static boolean haveSameParentOrDirectParentRelationship(ApplicationModule source, ApplicationModule target, + ApplicationModules modules) { + + var sourceParent = modules.getParentOf(source); + var targetParent = modules.getParentOf(target); + + // Top-level modules + return targetParent.isEmpty() + + // One is parent of the other + || hasValue(sourceParent, target) + || hasValue(targetParent, source) + + // Same immediate parent + || sourceParent.flatMap(it -> targetParent.filter(it::equals)).isPresent(); + } + + private static boolean hasValue(Optional optional, T expected) { + return optional.filter(expected::equals).isPresent(); + } } private static class InjectionDependency extends QualifiedDependency { diff --git a/spring-modulith-core/src/test/java/example/ni/nested/b/second/InNestedBSecond.java b/spring-modulith-core/src/test/java/example/ni/nested/b/second/InNestedBSecond.java index 16746458..d53489ea 100644 --- a/spring-modulith-core/src/test/java/example/ni/nested/b/second/InNestedBSecond.java +++ b/spring-modulith-core/src/test/java/example/ni/nested/b/second/InNestedBSecond.java @@ -16,10 +16,12 @@ package example.ni.nested.b.second; import example.ni.nested.InNested; +import example.ni.nested.b.first.InNestedBFirst; /** * @author Oliver Drotbohm */ public class InNestedBSecond { InNested inNested; + InNestedBFirst siblingReference; } diff --git a/src/docs/antora/modules/ROOT/pages/fundamentals.adoc b/src/docs/antora/modules/ROOT/pages/fundamentals.adoc index e9d10ce2..34aab768 100644 --- a/src/docs/antora/modules/ROOT/pages/fundamentals.adoc +++ b/src/docs/antora/modules/ROOT/pages/fundamentals.adoc @@ -211,7 +211,7 @@ In this example `inventory` is an application module as described xref:fundament The `@ApplicationModule` annotation on the `nested` package caused that to become a nested application module in turn. In that arrangement, the following access rules apply: -* The code in _Nested_ is only available from _Inventory_, i.e. only any of the `SomethingInventoryInternal` types can access `NestedApi`. `NestedInternal` is only accessible from `NestedApi`. +* The code in _Nested_ is only available from _Inventory_ or any types exposed by sibling application modules nested inside _Inventory_. * Any code in the _Nested_ module can access code in parent modules, even internal. I.e., both `NestedApi` and `NestedInternal` can access `inventory.internal.SomethingInventoryInternal`. * Code from nested modules can also access exposed types by top-level application modules.