From 22564796be4257ec09ed52820184b90c655df01e Mon Sep 17 00:00:00 2001 From: Shitikanth Kashyap Date: Sat, 21 Feb 2026 12:59:06 +0530 Subject: [PATCH] Add requireDependencyManagement enforcer rule Introduces a new rule that fails the build when any dependency has its version declared inline rather than via . Supports an parameter with glob patterns (groupId:artifactId, * wildcards anywhere) to exempt dependencies such as sibling modules in multi-module builds. Includes unit tests, integration tests for single-module and multi-module scenarios, and README documentation. --- README.md | 66 +++++++-- .../invoker.properties | 1 + .../pom.xml | 36 +++++ .../verify.groovy | 7 + .../invoker.properties | 1 + .../module-a/pom.xml | 9 ++ .../module-b/pom.xml | 17 +++ .../pom.xml | 37 +++++ .../verify.groovy | 4 + .../invoker.properties | 1 + .../pom.xml | 45 +++++++ .../verify.groovy | 2 + .../RequireDependencyManagement.java | 94 +++++++++++++ .../RequireDependencyManagementTest.java | 127 ++++++++++++++++++ 14 files changed, 436 insertions(+), 11 deletions(-) create mode 100644 src/it/fail-require-dependency-management/invoker.properties create mode 100644 src/it/fail-require-dependency-management/pom.xml create mode 100644 src/it/fail-require-dependency-management/verify.groovy create mode 100644 src/it/success-require-dependency-management-multimodule/invoker.properties create mode 100644 src/it/success-require-dependency-management-multimodule/module-a/pom.xml create mode 100644 src/it/success-require-dependency-management-multimodule/module-b/pom.xml create mode 100644 src/it/success-require-dependency-management-multimodule/pom.xml create mode 100644 src/it/success-require-dependency-management-multimodule/verify.groovy create mode 100644 src/it/success-require-dependency-management/invoker.properties create mode 100644 src/it/success-require-dependency-management/pom.xml create mode 100644 src/it/success-require-dependency-management/verify.groovy create mode 100644 src/main/java/io/github/shitikanth/enforcerrules/RequireDependencyManagement.java create mode 100644 src/test/java/io/github/shitikanth/enforcerrules/RequireDependencyManagementTest.java diff --git a/README.md b/README.md index 99059ce..f86506a 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,20 @@ [![Apache License, Version 2.0, January 2004](https://img.shields.io/github/license/shitikanth/enforcer-rules.svg?label=License)](http://www.apache.org/licenses/) [![Github CI](https://github.com/shitikanth/enforcer-rules/actions/workflows/ci.yml/badge.svg)](https://github.com/mojohaus/extra-enforcer-rules/actions/workflows/ci.yml) [![Maven Central](https://img.shields.io/maven-central/v/io.github.shitikanth/enforcer-rules.svg?label=Maven%20Central)](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")); + } +}