From 774327213bef52e4c1dcc85312159eb8542b7ec6 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:55:22 +0000 Subject: [PATCH 1/3] Add BOM import scope enforcer rule Implements a custom Maven Enforcer rule (bomImportScopeRule) that ensures dependencies with type=pom (BOMs) always use scope=import. - BOMs in are always flagged (they belong in dependencyManagement) - BOMs in without scope=import are flagged - Clear error messages listing all violations - Includes 9 unit tests covering all scenarios Co-Authored-By: lithium147@gmail.com --- .../solubris/enforcer/BomImportScopeRule.java | 107 ++++++++++++ .../enforcer/BomImportScopeRuleTest.java | 152 ++++++++++++++++++ 2 files changed, 259 insertions(+) create mode 100644 src/main/java/com/solubris/enforcer/BomImportScopeRule.java create mode 100644 src/test/java/com/solubris/enforcer/BomImportScopeRuleTest.java diff --git a/src/main/java/com/solubris/enforcer/BomImportScopeRule.java b/src/main/java/com/solubris/enforcer/BomImportScopeRule.java new file mode 100644 index 0000000..5b6f76b --- /dev/null +++ b/src/main/java/com/solubris/enforcer/BomImportScopeRule.java @@ -0,0 +1,107 @@ +package com.solubris.enforcer; + +import org.apache.maven.enforcer.rule.api.AbstractEnforcerRule; +import org.apache.maven.enforcer.rule.api.EnforcerRuleException; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.model.Dependency; +import org.apache.maven.model.DependencyManagement; +import org.apache.maven.model.Model; +import org.apache.maven.project.MavenProject; + +import javax.inject.Inject; +import javax.inject.Named; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * A custom Maven Enforcer rule that ensures all dependencies declared with + * {@code type=pom} (i.e. BOMs) also specify {@code scope=import}. + * + *

This rule inspects both the top-level {@code } section and + * the {@code } section of the project. Any dependency + * with {@code type=pom} that does not have {@code scope=import} will cause + * a build failure. + * + *

BOMs (Bill of Materials) are intended to be imported via + * {@code import} inside {@code }. + * Using them without import scope is almost always a mistake and can lead + * to unexpected transitive dependency resolution. + */ +@Named("bomImportScopeRule") +public class BomImportScopeRule extends AbstractEnforcerRule { + + private static final String POM_TYPE = "pom"; + private static final String IMPORT_SCOPE = "import"; + + private final Model model; + + @SuppressWarnings("unused") + @Inject + public BomImportScopeRule(MavenSession session) { + this(modelFrom(session)); + } + + protected BomImportScopeRule(Model model) { + this.model = model; + } + + private static Model modelFrom(MavenSession session) { + MavenProject project = session.getCurrentProject(); + Model originalModel = project.getOriginalModel(); + return originalModel != null ? originalModel : project.getModel(); + } + + @Override + public void execute() throws EnforcerRuleException { + List violations = scanAll().collect(Collectors.toList()); + + if (!violations.isEmpty()) { + String message = violations.stream() + .map(v -> " - " + v) + .collect(Collectors.joining("\n", + "BOM dependencies (type=pom) must use scope=import. " + + "Found " + violations.size() + " violation(s):\n", + "\n")); + throw new EnforcerRuleException(message); + } + + getLog().info("All BOM dependencies (type=pom) correctly use scope=import."); + } + + protected Stream scanAll() { + Stream topLevelViolations = directDependencies() + .filter(dep -> POM_TYPE.equals(dep.getType())) + .map(dep -> formatViolation(dep, "dependencies")); + + Stream depMgmtViolations = managedDependencies() + .filter(dep -> POM_TYPE.equals(dep.getType())) + .filter(dep -> !IMPORT_SCOPE.equals(dep.getScope())) + .map(dep -> formatViolation(dep, "dependencyManagement")); + + return Stream.concat(topLevelViolations, depMgmtViolations); + } + + private Stream directDependencies() { + return Optional.ofNullable(model.getDependencies()) + .stream() + .flatMap(Collection::stream); + } + + private Stream managedDependencies() { + return Optional.ofNullable(model.getDependencyManagement()) + .map(DependencyManagement::getDependencies) + .stream() + .flatMap(Collection::stream); + } + + private static String formatViolation(Dependency dep, String section) { + String scope = dep.getScope(); + String scopeInfo = (scope == null || scope.isEmpty()) ? "no scope" : "scope=" + scope; + return dep.getGroupId() + ":" + dep.getArtifactId() + ":" + dep.getVersion() + + " in <" + section + "> has type=pom but " + scopeInfo + + " (expected scope=import)"; + } +} diff --git a/src/test/java/com/solubris/enforcer/BomImportScopeRuleTest.java b/src/test/java/com/solubris/enforcer/BomImportScopeRuleTest.java new file mode 100644 index 0000000..82266d0 --- /dev/null +++ b/src/test/java/com/solubris/enforcer/BomImportScopeRuleTest.java @@ -0,0 +1,152 @@ +package com.solubris.enforcer; + +import org.apache.maven.enforcer.rule.api.EnforcerLogger; +import org.apache.maven.enforcer.rule.api.EnforcerRuleException; +import org.apache.maven.model.Dependency; +import org.apache.maven.model.DependencyManagement; +import org.apache.maven.model.Model; +import org.junit.jupiter.api.Test; + +import java.util.stream.Stream; + +import static com.solubris.enforcer.ModelStubber.dependencyOf; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; + +class BomImportScopeRuleTest { + + private final Model model = new Model(); + private final BomImportScopeRule rule = new BomImportScopeRule(model); + + BomImportScopeRuleTest() { + rule.setLog(mock(EnforcerLogger.class)); + } + + @Test + void noDependenciesPasses() { + assertThatNoException().isThrownBy(rule::execute); + } + + @Test + void jarDependencyPasses() { + model.addDependency(dependencyOf("com.example", "some-lib", "1.0")); + + assertThatNoException().isThrownBy(rule::execute); + } + + @Test + void bomWithImportScopeInDepMgmtPasses() { + Dependency bom = bomDependency("com.example", "some-bom", "1.0", "import"); + DependencyManagement depMgmt = new DependencyManagement(); + depMgmt.addDependency(bom); + model.setDependencyManagement(depMgmt); + + assertThatNoException().isThrownBy(rule::execute); + } + + @Test + void bomWithoutImportScopeInDepMgmtFails() { + Dependency bom = bomDependency("com.example", "some-bom", "1.0", "compile"); + DependencyManagement depMgmt = new DependencyManagement(); + depMgmt.addDependency(bom); + model.setDependencyManagement(depMgmt); + + Stream violations = rule.scanAll(); + + assertThat(violations) + .hasSize(1) + .first() + .asString() + .contains("com.example:some-bom:1.0") + .contains("scope=compile") + .contains("dependencyManagement"); + } + + @Test + void bomWithNoScopeInDepMgmtFails() { + Dependency bom = bomDependency("com.example", "some-bom", "1.0", null); + DependencyManagement depMgmt = new DependencyManagement(); + depMgmt.addDependency(bom); + model.setDependencyManagement(depMgmt); + + Stream violations = rule.scanAll(); + + assertThat(violations) + .hasSize(1) + .first() + .asString() + .contains("com.example:some-bom:1.0") + .contains("no scope"); + } + + @Test + void bomInTopLevelDependenciesFails() { + Dependency bom = bomDependency("com.example", "some-bom", "1.0", "import"); + model.addDependency(bom); + + Stream violations = rule.scanAll(); + + assertThat(violations) + .hasSize(1) + .first() + .asString() + .contains("com.example:some-bom:1.0") + .contains(""); + } + + @Test + void multipleViolationsReported() { + // BOM in top-level dependencies + Dependency topBom = bomDependency("com.example", "top-bom", "1.0", "compile"); + model.addDependency(topBom); + + // BOM in depMgmt without import scope + Dependency mgmtBom = bomDependency("com.example", "mgmt-bom", "2.0", "runtime"); + DependencyManagement depMgmt = new DependencyManagement(); + depMgmt.addDependency(mgmtBom); + model.setDependencyManagement(depMgmt); + + Stream violations = rule.scanAll(); + + assertThat(violations) + .hasSize(2) + .anyMatch(v -> v.contains("com.example:top-bom:1.0")) + .anyMatch(v -> v.contains("com.example:mgmt-bom:2.0")); + } + + @Test + void mixedDependenciesWithValidBomPasses() { + // Non-pom dependency in top-level deps + model.addDependency(dependencyOf("com.example", "some-lib", "1.0")); + + // Valid BOM in depMgmt + Dependency bom = bomDependency("com.example", "some-bom", "1.0", "import"); + DependencyManagement depMgmt = new DependencyManagement(); + depMgmt.addDependency(bom); + model.setDependencyManagement(depMgmt); + + assertThatNoException().isThrownBy(rule::execute); + } + + @Test + void executeThrowsWithViolationMessage() { + Dependency bom = bomDependency("com.example", "bad-bom", "3.0", "compile"); + DependencyManagement depMgmt = new DependencyManagement(); + depMgmt.addDependency(bom); + model.setDependencyManagement(depMgmt); + + assertThatThrownBy(rule::execute) + .isInstanceOf(EnforcerRuleException.class) + .hasMessageContaining("com.example:bad-bom:3.0") + .hasMessageContaining("1 violation(s)"); + } + + private static Dependency bomDependency(String groupId, String artifactId, String version, String scope) { + Dependency dep = dependencyOf(groupId, artifactId, version); + dep.setType("pom"); + dep.setScope(scope); + return dep; + } +} From 5afc5bcf123d07b31a59170d909aacde701b8bfa Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:18:20 +0000 Subject: [PATCH 2/3] Fix PMD CommentDefaultAccessModifier warning in BomImportScopeRuleTest Co-Authored-By: lithium147@gmail.com --- src/test/java/com/solubris/enforcer/BomImportScopeRuleTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/solubris/enforcer/BomImportScopeRuleTest.java b/src/test/java/com/solubris/enforcer/BomImportScopeRuleTest.java index 82266d0..944f5b0 100644 --- a/src/test/java/com/solubris/enforcer/BomImportScopeRuleTest.java +++ b/src/test/java/com/solubris/enforcer/BomImportScopeRuleTest.java @@ -20,7 +20,7 @@ class BomImportScopeRuleTest { private final Model model = new Model(); private final BomImportScopeRule rule = new BomImportScopeRule(model); - BomImportScopeRuleTest() { + /* default */ BomImportScopeRuleTest() { rule.setLog(mock(EnforcerLogger.class)); } From f944a61c9577d2f1032059ffcb144a2684efa12f Mon Sep 17 00:00:00 2001 From: twalters Date: Wed, 4 Mar 2026 23:57:18 +0000 Subject: [PATCH 3/3] clean up --- .../solubris/enforcer/BomImportScopeRule.java | 31 +++++++++---------- .../enforcer/BomImportScopeRuleTest.java | 3 +- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/solubris/enforcer/BomImportScopeRule.java b/src/main/java/com/solubris/enforcer/BomImportScopeRule.java index 5b6f76b..e222d18 100644 --- a/src/main/java/com/solubris/enforcer/BomImportScopeRule.java +++ b/src/main/java/com/solubris/enforcer/BomImportScopeRule.java @@ -11,11 +11,14 @@ import javax.inject.Inject; import javax.inject.Named; import java.util.Collection; -import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; +import java.util.concurrent.atomic.LongAdder; import java.util.stream.Stream; +import static java.text.MessageFormat.format; +import static java.util.stream.Collectors.joining; +import static org.apache.maven.artifact.ArtifactUtils.key; + /** * A custom Maven Enforcer rule that ensures all dependencies declared with * {@code type=pom} (i.e. BOMs) also specify {@code scope=import}. @@ -32,7 +35,6 @@ */ @Named("bomImportScopeRule") public class BomImportScopeRule extends AbstractEnforcerRule { - private static final String POM_TYPE = "pom"; private static final String IMPORT_SCOPE = "import"; @@ -56,16 +58,14 @@ private static Model modelFrom(MavenSession session) { @Override public void execute() throws EnforcerRuleException { - List violations = scanAll().collect(Collectors.toList()); - - if (!violations.isEmpty()) { - String message = violations.stream() - .map(v -> " - " + v) - .collect(Collectors.joining("\n", - "BOM dependencies (type=pom) must use scope=import. " - + "Found " + violations.size() + " violation(s):\n", - "\n")); - throw new EnforcerRuleException(message); + LongAdder count = new LongAdder(); + String body = scanAll() + .peek(v -> count.increment()) + .map(v -> " - " + v) + .collect(joining("\n", "", "\n")); + if (count.longValue() > 0) { + String title = "BOM dependencies (type=pom) must use scope=import. Found " + count.sum() + " violation(s):"; + throw new EnforcerRuleException(title + "\n" + body); } getLog().info("All BOM dependencies (type=pom) correctly use scope=import."); @@ -100,8 +100,7 @@ private Stream managedDependencies() { private static String formatViolation(Dependency dep, String section) { String scope = dep.getScope(); String scopeInfo = (scope == null || scope.isEmpty()) ? "no scope" : "scope=" + scope; - return dep.getGroupId() + ":" + dep.getArtifactId() + ":" + dep.getVersion() - + " in <" + section + "> has type=pom but " + scopeInfo - + " (expected scope=import)"; + String key = key(dep.getGroupId(), dep.getArtifactId(), dep.getVersion()); + return format("{0} in <{1}> has type=pom but {2} (expected scope=import)", key, section, scopeInfo); } } diff --git a/src/test/java/com/solubris/enforcer/BomImportScopeRuleTest.java b/src/test/java/com/solubris/enforcer/BomImportScopeRuleTest.java index 944f5b0..10d5f7c 100644 --- a/src/test/java/com/solubris/enforcer/BomImportScopeRuleTest.java +++ b/src/test/java/com/solubris/enforcer/BomImportScopeRuleTest.java @@ -16,11 +16,10 @@ import static org.mockito.Mockito.mock; class BomImportScopeRuleTest { - private final Model model = new Model(); private final BomImportScopeRule rule = new BomImportScopeRule(model); - /* default */ BomImportScopeRuleTest() { + BomImportScopeRuleTest() { rule.setLog(mock(EnforcerLogger.class)); }