diff --git a/README.md b/README.md
index 99059ce..f86506a 100644
--- a/README.md
+++ b/README.md
@@ -1,20 +1,20 @@
[](http://www.apache.org/licenses/) [](https://github.com/mojohaus/extra-enforcer-rules/actions/workflows/ci.yml)
[](https://search.maven.org/artifact/io.github.shitikanth/enforcer-rules)
-# Motivation
-## Ban Empty Java Files
+# Enforcer Rules
-Empty Java source files (or all commented or just not containing a class with the same name) are
-detected as stale source files
-by [Apache Maven Compiler Plugin](https://maven.apache.org/plugins/maven-compiler-plugin/), so modules containing such
-files get unnecessarily recompiled every single time.
+Custom [Maven Enforcer](https://maven.apache.org/enforcer/maven-enforcer-plugin/) rules.
-This plugin adds an enforcer rule to detect and ban such files.
+## Rules
+
+- [banEmptyJavaFiles](#banemptyjavafiles)
+- [requireDependencyManagement](#requiredependencymanagement)
# Usage
-```xml
+Add `enforcer-rules` as a dependency of the `maven-enforcer-plugin` and configure the desired rules inside an `enforce` execution:
+```xml
org.apache.maven.plugins
maven-enforcer-plugin
@@ -23,21 +23,65 @@ This plugin adds an enforcer rule to detect and ban such files.
io.github.shitikanth
enforcer-rules
- 1.0-SNAPSHOT
+ 1.0.4
- enforce-java-rules
+ enforce
enforce
+
-```
\ No newline at end of file
+```
+
+---
+
+## banEmptyJavaFiles
+
+Empty Java source files — or files that contain no top-level type declaration whose name matches the file name — are detected as stale by the [Maven Compiler Plugin](https://maven.apache.org/plugins/maven-compiler-plugin/), causing unnecessary recompilation on every build.
+
+This rule fails the build if any such file is found.
+
+```xml
+
+```
+
+---
+
+## requireDependencyManagement
+
+Fails the build if any project dependency declares its version inline rather than inheriting it from ``. This encourages centralised version management and prevents version drift across modules.
+
+```xml
+
+```
+
+### Parameters
+
+| Parameter | Type | Default | Description |
+|---|---|---|---|
+| `excludes` | `List` | *(empty)* | Dependency coordinates to exempt from the check, in `groupId:artifactId` format. `*` is a glob wildcard and may appear anywhere within either segment. |
+
+### Excluding dependencies
+
+Use `` to exempt specific dependencies. The most common use case is a multi-module project where modules depend on each other with `${project.version}` — these do not need a `` entry.
+
+```xml
+
+
+
+ ${project.groupId}:*
+
+
+```
+
+
diff --git a/src/it/fail-require-dependency-management/invoker.properties b/src/it/fail-require-dependency-management/invoker.properties
new file mode 100644
index 0000000..c21e972
--- /dev/null
+++ b/src/it/fail-require-dependency-management/invoker.properties
@@ -0,0 +1 @@
+invoker.buildResult = failure
diff --git a/src/it/fail-require-dependency-management/pom.xml b/src/it/fail-require-dependency-management/pom.xml
new file mode 100644
index 0000000..5f28e27
--- /dev/null
+++ b/src/it/fail-require-dependency-management/pom.xml
@@ -0,0 +1,36 @@
+
+ 4.0.0
+ io.github.shitikanth
+ fail-require-dependency-management
+ 1.0-SNAPSHOT
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.11.0
+ test
+
+
+
+
+
+
+ maven-enforcer-plugin
+ @maven-enforcer-plugin.version@
+
+
+ @project.groupId@
+ @project.artifactId@
+ @project.version@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/it/fail-require-dependency-management/verify.groovy b/src/it/fail-require-dependency-management/verify.groovy
new file mode 100644
index 0000000..7dba8d2
--- /dev/null
+++ b/src/it/fail-require-dependency-management/verify.groovy
@@ -0,0 +1,7 @@
+File file = new File( basedir, "build.log" )
+assert file.exists()
+String text = file.getText("utf-8")
+
+assert text.contains('[ERROR] Rule 0: io.github.shitikanth.enforcerrules.RequireDependencyManagement failed with message:')
+assert text.contains('[ERROR] Dependencies without dependency management:')
+assert text.contains('\t- org.junit.jupiter:junit-jupiter:5.11.0')
diff --git a/src/it/success-require-dependency-management-multimodule/invoker.properties b/src/it/success-require-dependency-management-multimodule/invoker.properties
new file mode 100644
index 0000000..4ab41c6
--- /dev/null
+++ b/src/it/success-require-dependency-management-multimodule/invoker.properties
@@ -0,0 +1 @@
+invoker.buildResult = success
diff --git a/src/it/success-require-dependency-management-multimodule/module-a/pom.xml b/src/it/success-require-dependency-management-multimodule/module-a/pom.xml
new file mode 100644
index 0000000..6773d57
--- /dev/null
+++ b/src/it/success-require-dependency-management-multimodule/module-a/pom.xml
@@ -0,0 +1,9 @@
+
+ 4.0.0
+
+ io.github.shitikanth.it
+ success-require-dependency-management-multimodule
+ 1.0-SNAPSHOT
+
+ module-a
+
diff --git a/src/it/success-require-dependency-management-multimodule/module-b/pom.xml b/src/it/success-require-dependency-management-multimodule/module-b/pom.xml
new file mode 100644
index 0000000..9cc9bee
--- /dev/null
+++ b/src/it/success-require-dependency-management-multimodule/module-b/pom.xml
@@ -0,0 +1,17 @@
+
+ 4.0.0
+
+ io.github.shitikanth.it
+ success-require-dependency-management-multimodule
+ 1.0-SNAPSHOT
+
+ module-b
+
+
+
+ io.github.shitikanth.it
+ module-a
+ ${project.version}
+
+
+
diff --git a/src/it/success-require-dependency-management-multimodule/pom.xml b/src/it/success-require-dependency-management-multimodule/pom.xml
new file mode 100644
index 0000000..f952d2a
--- /dev/null
+++ b/src/it/success-require-dependency-management-multimodule/pom.xml
@@ -0,0 +1,37 @@
+
+ 4.0.0
+ io.github.shitikanth.it
+ success-require-dependency-management-multimodule
+ 1.0-SNAPSHOT
+ pom
+
+
+ module-a
+ module-b
+
+
+
+
+
+ maven-enforcer-plugin
+ @maven-enforcer-plugin.version@
+
+
+ @project.groupId@
+ @project.artifactId@
+ @project.version@
+
+
+
+
+
+
+ ${project.groupId}:*
+
+
+
+
+
+
+
+
diff --git a/src/it/success-require-dependency-management-multimodule/verify.groovy b/src/it/success-require-dependency-management-multimodule/verify.groovy
new file mode 100644
index 0000000..3287ba4
--- /dev/null
+++ b/src/it/success-require-dependency-management-multimodule/verify.groovy
@@ -0,0 +1,4 @@
+File file = new File( basedir, "build.log" )
+assert file.exists()
+String text = file.getText("utf-8")
+assert !text.contains('Dependencies without dependency management:')
diff --git a/src/it/success-require-dependency-management/invoker.properties b/src/it/success-require-dependency-management/invoker.properties
new file mode 100644
index 0000000..4ab41c6
--- /dev/null
+++ b/src/it/success-require-dependency-management/invoker.properties
@@ -0,0 +1 @@
+invoker.buildResult = success
diff --git a/src/it/success-require-dependency-management/pom.xml b/src/it/success-require-dependency-management/pom.xml
new file mode 100644
index 0000000..bf5d7fd
--- /dev/null
+++ b/src/it/success-require-dependency-management/pom.xml
@@ -0,0 +1,45 @@
+
+ 4.0.0
+ io.github.shitikanth
+ success-require-dependency-management
+ 1.0-SNAPSHOT
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.11.0
+
+
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+
+
+
+
+ maven-enforcer-plugin
+ @maven-enforcer-plugin.version@
+
+
+ @project.groupId@
+ @project.artifactId@
+ @project.version@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/it/success-require-dependency-management/verify.groovy b/src/it/success-require-dependency-management/verify.groovy
new file mode 100644
index 0000000..dac72e9
--- /dev/null
+++ b/src/it/success-require-dependency-management/verify.groovy
@@ -0,0 +1,2 @@
+File file = new File( basedir, "build.log" )
+assert file.exists()
diff --git a/src/main/java/io/github/shitikanth/enforcerrules/RequireDependencyManagement.java b/src/main/java/io/github/shitikanth/enforcerrules/RequireDependencyManagement.java
new file mode 100644
index 0000000..5d71830
--- /dev/null
+++ b/src/main/java/io/github/shitikanth/enforcerrules/RequireDependencyManagement.java
@@ -0,0 +1,94 @@
+package io.github.shitikanth.enforcerrules;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.apache.maven.enforcer.rule.api.AbstractEnforcerRule;
+import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
+import org.apache.maven.model.Dependency;
+import org.apache.maven.model.DependencyManagement;
+import org.apache.maven.project.MavenProject;
+
+@Named("requireDependencyManagement")
+class RequireDependencyManagement extends AbstractEnforcerRule {
+
+ private final MavenProject project;
+
+ private List excludes = new ArrayList<>();
+
+ @Inject
+ public RequireDependencyManagement(MavenProject project) {
+ this.project = project;
+ }
+
+ void setExcludes(List excludes) {
+ this.excludes = excludes;
+ }
+
+ @Override
+ public void execute() throws EnforcerRuleException {
+
+ Set managedKeys = new HashSet<>();
+ DependencyManagement dependencyManagement = project.getDependencyManagement();
+ if (dependencyManagement != null) {
+ for (Dependency managed : dependencyManagement.getDependencies()) {
+ managedKeys.add(managed.getGroupId() + ":" + managed.getArtifactId());
+ }
+ }
+
+ List violations = new ArrayList<>();
+ for (Dependency dep : project.getDependencies()) {
+ String key = dep.getGroupId() + ":" + dep.getArtifactId();
+ if (!managedKeys.contains(key) && !isExcluded(dep.getGroupId(), dep.getArtifactId())) {
+ violations.add(dep);
+ }
+ }
+
+ if (!violations.isEmpty()) {
+ StringBuilder sb = new StringBuilder("Dependencies without dependency management:\n");
+ for (Dependency dep : violations) {
+ sb.append("\t- ")
+ .append(dep.getGroupId())
+ .append(":")
+ .append(dep.getArtifactId())
+ .append(":")
+ .append(dep.getVersion())
+ .append("\n");
+ }
+ throw new EnforcerRuleException(sb.toString());
+ }
+ }
+
+ private boolean isExcluded(String groupId, String artifactId) {
+ for (String exclude : excludes) {
+ int colon = exclude.indexOf(':');
+ if (colon < 0) {
+ continue;
+ }
+ String groupPattern = exclude.substring(0, colon);
+ String artifactPattern = exclude.substring(colon + 1);
+ if (matchesGlob(groupPattern, groupId) && matchesGlob(artifactPattern, artifactId)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean matchesGlob(String pattern, String input) {
+ String[] parts = pattern.split("\\*", -1);
+ StringBuilder regex = new StringBuilder("^");
+ for (int i = 0; i < parts.length; i++) {
+ if (i > 0) {
+ regex.append(".*");
+ }
+ regex.append(java.util.regex.Pattern.quote(parts[i]));
+ }
+ regex.append("$");
+ return input.matches(regex.toString());
+ }
+}
diff --git a/src/test/java/io/github/shitikanth/enforcerrules/RequireDependencyManagementTest.java b/src/test/java/io/github/shitikanth/enforcerrules/RequireDependencyManagementTest.java
new file mode 100644
index 0000000..4c76c6c
--- /dev/null
+++ b/src/test/java/io/github/shitikanth/enforcerrules/RequireDependencyManagementTest.java
@@ -0,0 +1,127 @@
+package io.github.shitikanth.enforcerrules;
+
+import java.util.List;
+
+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 org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class RequireDependencyManagementTest {
+
+ private static Dependency dependency(String groupId, String artifactId, String version) {
+ var dep = new Dependency();
+ dep.setGroupId(groupId);
+ dep.setArtifactId(artifactId);
+ dep.setVersion(version);
+ return dep;
+ }
+
+ private static MavenProject projectWithManagedDep(String groupId, String artifactId) {
+ var managed = new Dependency();
+ managed.setGroupId(groupId);
+ managed.setArtifactId(artifactId);
+ managed.setVersion("1.0");
+
+ var depMgmt = new DependencyManagement();
+ depMgmt.setDependencies(List.of(managed));
+
+ var model = new Model();
+ model.setDependencyManagement(depMgmt);
+ return new MavenProject(model);
+ }
+
+ @Test
+ void allDepsManaged_passes() {
+ var project = projectWithManagedDep("org.example", "foo");
+ project.setDependencies(List.of(dependency("org.example", "foo", "1.0")));
+
+ var rule = new RequireDependencyManagement(project);
+ assertDoesNotThrow(rule::execute);
+ }
+
+ @Test
+ void unmanagedDep_fails() {
+ var project = new MavenProject();
+ project.setDependencies(List.of(dependency("org.example", "bar", "2.0")));
+
+ var rule = new RequireDependencyManagement(project);
+ var ex = assertThrows(Exception.class, rule::execute);
+ assertTrue(ex.getMessage().contains("Dependencies without dependency management:"));
+ assertTrue(ex.getMessage().contains("\t- org.example:bar:2.0"));
+ }
+
+ @Test
+ void noDepsAndNoDependencyManagement_passes() {
+ var project = new MavenProject();
+
+ var rule = new RequireDependencyManagement(project);
+ assertDoesNotThrow(rule::execute);
+ }
+
+ @Test
+ void multipleDeps_onlyUnmanagedListed() {
+ var project = projectWithManagedDep("org.example", "foo");
+ project.setDependencies(
+ List.of(dependency("org.example", "foo", "1.0"), dependency("org.example", "bar", "2.0")));
+
+ var rule = new RequireDependencyManagement(project);
+ var ex = assertThrows(Exception.class, rule::execute);
+ assertFalse(ex.getMessage().contains("org.example:foo"));
+ assertTrue(ex.getMessage().contains("\t- org.example:bar:2.0"));
+ }
+
+ @Test
+ void exactExclude_passes() {
+ var project = new MavenProject();
+ project.setDependencies(List.of(dependency("org.example", "bar", "2.0")));
+
+ var rule = new RequireDependencyManagement(project);
+ rule.setExcludes(List.of("org.example:bar"));
+ assertDoesNotThrow(rule::execute);
+ }
+
+ @Test
+ void wildcardArtifact_passes() {
+ var project = new MavenProject();
+ project.setDependencies(List.of(dependency("org.example", "bar", "2.0")));
+
+ var rule = new RequireDependencyManagement(project);
+ rule.setExcludes(List.of("org.example:*"));
+ assertDoesNotThrow(rule::execute);
+ }
+
+ @Test
+ void wildcardGroup_passes() {
+ var project = new MavenProject();
+ project.setDependencies(List.of(dependency("org.example", "bar", "2.0")));
+
+ var rule = new RequireDependencyManagement(project);
+ rule.setExcludes(List.of("*:bar"));
+ assertDoesNotThrow(rule::execute);
+ }
+
+ @Test
+ void midSegmentWildcard_passes() {
+ var project = new MavenProject();
+ project.setDependencies(List.of(dependency("org.example", "bar", "2.0")));
+
+ var rule = new RequireDependencyManagement(project);
+ rule.setExcludes(List.of("org.*:b*"));
+ assertDoesNotThrow(rule::execute);
+ }
+
+ @Test
+ void excludeMismatch_fails() {
+ var project = new MavenProject();
+ project.setDependencies(List.of(dependency("org.example", "bar", "2.0")));
+
+ var rule = new RequireDependencyManagement(project);
+ rule.setExcludes(List.of("org.example:other"));
+ var ex = assertThrows(Exception.class, rule::execute);
+ assertTrue(ex.getMessage().contains("\t- org.example:bar:2.0"));
+ }
+}