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 62e3cf990..64b1eabf4 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 @@ -1345,6 +1345,21 @@ Violations isValidDependencyWithin(ApplicationModules modules) { // Check explicitly defined allowed targets if (!allowed.isAllowedDependency(target)) { + // When the target module is itself listed as an allowed dependency (for example via + // sharedModules) but the concrete type is not part of the allowed named interfaces, + // report a non-exposed-type violation. Otherwise the "Allowed targets: " + // message is ambiguous (GH-1778). + var targetModuleAllowed = allowed.stream() + .anyMatch(it -> it.getTargetModule().equals(targetModule)); + + if (targetModuleAllowed && !targetModule.isExposed(target)) { + + var violationText = INTERNAL_REFERENCE + .formatted(originModule.getIdentifier(), target.getName(), targetModule.getIdentifier()); + + return violations.and(new Violation(violationText + lineSeparator() + description)); + } + var targetNamedInterfaces = targetModule.getNamedInterfaces() .getNamedInterfacesContaining(target) .filter(NamedInterface::isNamed) diff --git a/spring-modulith-core/src/test/java/reproducers/gh1778/Application.java b/spring-modulith-core/src/test/java/reproducers/gh1778/Application.java new file mode 100644 index 000000000..db3cfbc07 --- /dev/null +++ b/spring-modulith-core/src/test/java/reproducers/gh1778/Application.java @@ -0,0 +1,27 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package reproducers.gh1778; + +import org.springframework.modulith.Modulithic; + +/** + * Reproducer application for GH-1778. + * + * @author Burak Kalayci + */ +@Modulithic(sharedModules = "review") +public class Application { +} diff --git a/spring-modulith-core/src/test/java/reproducers/gh1778/Gh1778Tests.java b/spring-modulith-core/src/test/java/reproducers/gh1778/Gh1778Tests.java new file mode 100644 index 000000000..22313d2ba --- /dev/null +++ b/spring-modulith-core/src/test/java/reproducers/gh1778/Gh1778Tests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package reproducers.gh1778; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.modulith.core.ApplicationModules; +import org.springframework.modulith.core.Violations; + +import com.tngtech.archunit.core.importer.ImportOption; + +/** + * Regression tests for GH-1778. + * + * @author Burak Kalayci + */ +class Gh1778Tests { + + @Test // GH-1778 + void reportsNonExposedTypeWhenSharedModuleTargetIsNotExposed() { + + var modules = ApplicationModules.of(Application.class, new ImportOption.OnlyIncludeTests()); + + assertThat(modules.getSharedModules()) + .extracting(it -> it.getIdentifier().toString()) + .containsExactly("review"); + + assertThatExceptionOfType(Violations.class) // + .isThrownBy(modules::verify) // + .satisfies(ex -> assertThat(ex.getMessages()) + .anySatisfy(message -> assertThat(message) + .contains("Module 'bidding'") + .contains("non-exposed type") + .contains("reproducers.gh1778.review.analysis.AnalyzerService") + .contains("review") + .doesNotContain("Allowed targets"))); + } +} diff --git a/spring-modulith-core/src/test/java/reproducers/gh1778/bidding/BidService.java b/spring-modulith-core/src/test/java/reproducers/gh1778/bidding/BidService.java new file mode 100644 index 000000000..37905f6c7 --- /dev/null +++ b/spring-modulith-core/src/test/java/reproducers/gh1778/bidding/BidService.java @@ -0,0 +1,32 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package reproducers.gh1778.bidding; + +import reproducers.gh1778.review.analysis.AnalyzerService; + +/** + * Depends on a non-exposed type of the shared review module. + * + * @author Burak Kalayci + */ +public class BidService { + + private final AnalyzerService analyzerService; + + public BidService(AnalyzerService analyzerService) { + this.analyzerService = analyzerService; + } +} diff --git a/spring-modulith-core/src/test/java/reproducers/gh1778/bidding/package-info.java b/spring-modulith-core/src/test/java/reproducers/gh1778/bidding/package-info.java new file mode 100644 index 000000000..c6b759828 --- /dev/null +++ b/spring-modulith-core/src/test/java/reproducers/gh1778/bidding/package-info.java @@ -0,0 +1,2 @@ +@org.springframework.modulith.ApplicationModule(allowedDependencies = {}) +package reproducers.gh1778.bidding; diff --git a/spring-modulith-core/src/test/java/reproducers/gh1778/review/analysis/AnalyzerService.java b/spring-modulith-core/src/test/java/reproducers/gh1778/review/analysis/AnalyzerService.java new file mode 100644 index 000000000..434b11516 --- /dev/null +++ b/spring-modulith-core/src/test/java/reproducers/gh1778/review/analysis/AnalyzerService.java @@ -0,0 +1,24 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package reproducers.gh1778.review.analysis; + +/** + * Internal type of the review module (not part of the exposed API package). + * + * @author Burak Kalayci + */ +public class AnalyzerService { +} diff --git a/spring-modulith-core/src/test/java/reproducers/gh1778/review/package-info.java b/spring-modulith-core/src/test/java/reproducers/gh1778/review/package-info.java new file mode 100644 index 000000000..90ea24466 --- /dev/null +++ b/spring-modulith-core/src/test/java/reproducers/gh1778/review/package-info.java @@ -0,0 +1,2 @@ +@org.springframework.modulith.ApplicationModule(allowedDependencies = {}) +package reproducers.gh1778.review;