From ec185788a14cd0ce9dc0bb2e8d66b8382ec0812c Mon Sep 17 00:00:00 2001 From: Geert Bevin Date: Fri, 17 Jul 2026 17:18:33 -0400 Subject: [PATCH] Warn when declared versions differ from applicable BOMs and only report in download --- .../bld/dependencies/DependencyScopes.java | 34 +++++++++ .../bld/dependencies/VersionResolution.java | 72 +++++++++++++++++++ .../bld/operations/DownloadOperation.java | 17 +++++ .../rife/bld/operations/PurgeOperation.java | 19 ----- .../java/rife/bld/dependencies/TestBom.java | 42 +++++++++++ 5 files changed, 165 insertions(+), 19 deletions(-) diff --git a/src/main/java/rife/bld/dependencies/DependencyScopes.java b/src/main/java/rife/bld/dependencies/DependencyScopes.java index 3f4dbed..f1e9fb1 100644 --- a/src/main/java/rife/bld/dependencies/DependencyScopes.java +++ b/src/main/java/rife/bld/dependencies/DependencyScopes.java @@ -178,6 +178,40 @@ public class DependencyScopes extends LinkedHashMap { return result; } + /** + * Returns the declared dependencies whose explicit version differs + * from the version that a BOM applying to their scope manages them at. + *

+ * The declared version is used for the dependency itself, its + * transitive dependencies still resolve to the versions that the BOMs + * manage. Dependencies whose version is supplied by a + * {@code bld.override} property are not reported. Each difference is + * reported once. + * + * @param properties the properties to use to get artifacts + * @param retriever the retriever to use to get artifacts + * @param repositories the repositories to use for the BOM resolution + * @return the version differences between the declared dependencies + * and the applicable BOMs + * @since 2.4.0 + */ + public List declaredVersionConflicts(HierarchicalProperties properties, ArtifactRetriever retriever, List repositories) { + var result = new ArrayList(); + var seen = new HashSet(); + for (var entry : entrySet()) { + var effective_boms = effectiveBoms(entry.getKey()); + if (effective_boms.isEmpty()) { + continue; + } + for (var conflict : VersionResolution.resolveDeclaredVersionConflicts(properties, retriever, repositories, effective_boms, entry.getValue())) { + if (seen.add(conflict.dependency() + conflict.declaredVersion() + conflict.bom() + conflict.bomVersion())) { + result.add(conflict); + } + } + } + return result; + } + /** * Returns the transitive set of dependencies that would be used for the compile scope in a project. * diff --git a/src/main/java/rife/bld/dependencies/VersionResolution.java b/src/main/java/rife/bld/dependencies/VersionResolution.java index 8b4c65b..506d9aa 100644 --- a/src/main/java/rife/bld/dependencies/VersionResolution.java +++ b/src/main/java/rife/bld/dependencies/VersionResolution.java @@ -233,6 +233,78 @@ public class VersionResolution { return conflicts; } + /** + * Describes a declared dependency whose explicit version differs from + * the version that an applicable bill of materials manages it at. + *

+ * The declared version is used for the dependency itself, its + * transitive dependencies still resolve to the versions that the BOM + * manages. + * + * @param dependency the group and artifact identifiers of the + * declared dependency + * @param declaredVersion the version the dependency is declared with + * @param bom the BOM that manages the dependency, the one + * with the highest precedence when several do + * @param bomVersion the version the BOM manages the dependency at + * @since 2.4.0 + */ + public record DeclaredVersionConflict(String dependency, Version declaredVersion, String bom, Version bomVersion) { + } + + /** + * Finds the declared dependencies whose explicit version differs from + * the version that the provided bills of materials manage them at. + *

+ * Dependencies that are declared without a version or whose version is + * supplied by a {@code bld.override} property are not reported. + * + * @param properties the properties to use to get artifacts + * @param retriever the retriever to use to get the BOMs + * @param repositories the repositories to resolve the BOMs in + * @param boms the BOMs to check, in precedence order + * @param declared the declared dependencies to check + * @return the version differences between the declared dependencies and the BOMs + * @since 2.4.0 + */ + public static List resolveDeclaredVersionConflicts(HierarchicalProperties properties, ArtifactRetriever retriever, List repositories, Collection boms, Collection declared) { + var base = new VersionResolution(properties); + var managed_versions = new LinkedHashMap(); + var managed_boms = new LinkedHashMap(); + if (boms != null) { + for (var bom : boms) { + var pom = new DependencyResolver(base, retriever, repositories, bom).getMavenPom(bom); + for (var managed : pom.getManagedDependencies()) { + if (managed.version() != null && !managed.version().isBlank()) { + var dependency = managed.convertToDependency(); + var key = managedKey(dependency); + // the first BOM that manages a dependency determines + // its version, mirroring the resolution precedence + if (managed_versions.putIfAbsent(key, dependency.version()) == null) { + managed_boms.put(key, bom.toArtifactString()); + } + } + } + } + } + + var conflicts = new ArrayList(); + if (declared != null) { + for (var dependency : declared) { + if (VersionNumber.UNKNOWN.equals(dependency.version()) || + base.versionOverrides_.containsKey(dependency.toArtifactString())) { + continue; + } + var key = managedKey(dependency); + var managed_version = managed_versions.get(key); + if (managed_version != null && !managed_version.equals(dependency.version())) { + conflicts.add(new DeclaredVersionConflict(dependency.toArtifactString(), dependency.version(), managed_boms.get(key), managed_version)); + } + } + } + return conflicts; + } + // builds the identity that dependency management entries are matched // on, mirroring Maven this includes the type and the classifier, the // modular and forced-classpath JAR types match the plain jar entries diff --git a/src/main/java/rife/bld/operations/DownloadOperation.java b/src/main/java/rife/bld/operations/DownloadOperation.java index 51d16dd..19991a7 100644 --- a/src/main/java/rife/bld/operations/DownloadOperation.java +++ b/src/main/java/rife/bld/operations/DownloadOperation.java @@ -89,6 +89,9 @@ public class DownloadOperation extends AbstractOperation { for (var conflict : dependencies().bomVersionConflicts(properties(), artifactRetriever(), repositories())) { System.out.println(formatBomVersionConflict(conflict)); } + for (var conflict : dependencies().declaredVersionConflicts(properties(), artifactRetriever(), repositories())) { + System.out.println(formatDeclaredVersionConflict(conflict)); + } } /** @@ -110,6 +113,20 @@ public class DownloadOperation extends AbstractOperation { return message.toString(); } + /** + * Formats a warning message for a declared dependency whose version + * differs from the version that a BOM manages it at. + * + * @param conflict the declared version conflict to format + * @return the formatted warning message + * @since 2.4.0 + */ + protected static String formatDeclaredVersionConflict(rife.bld.dependencies.VersionResolution.DeclaredVersionConflict conflict) { + return "Warning: '" + conflict.dependency() + "' is declared with version " + conflict.declaredVersion() + + " while BOM '" + conflict.bom() + "' manages it at " + conflict.bomVersion() + + ", the declared version is used but transitive dependencies still follow the BOM"; + } + /** * Part of the {@link #execute} operation, download the {@code compile} scope artifacts. * diff --git a/src/main/java/rife/bld/operations/PurgeOperation.java b/src/main/java/rife/bld/operations/PurgeOperation.java index ef63f01..627319c 100644 --- a/src/main/java/rife/bld/operations/PurgeOperation.java +++ b/src/main/java/rife/bld/operations/PurgeOperation.java @@ -54,7 +54,6 @@ public class PurgeOperation extends AbstractOperation { return; } - executeReportUncoveredDependencies(); executePurgeCompileDependencies(); executePurgeProvidedDependencies(); executePurgeRuntimeDependencies(); @@ -65,24 +64,6 @@ public class PurgeOperation extends AbstractOperation { } } - /** - * Part of the {@link #execute} operation, warns about version-less - * dependencies that are not covered by a BOM in their scope. - * - * @since 2.4.0 - */ - protected void executeReportUncoveredDependencies() { - if (silent()) { - return; - } - for (var dependency : dependencies().versionlessDependenciesWithoutBom(properties(), artifactRetriever(), repositories())) { - System.out.println("Warning: '" + dependency.toArtifactString() + "' isn't covered by a BOM, its latest version will be used"); - } - for (var conflict : dependencies().bomVersionConflicts(properties(), artifactRetriever(), repositories())) { - System.out.println(DownloadOperation.formatBomVersionConflict(conflict)); - } - } - /** * Part of the {@link #execute} operation, purge the {@code compile} scope artifacts. * diff --git a/src/test/java/rife/bld/dependencies/TestBom.java b/src/test/java/rife/bld/dependencies/TestBom.java index cc15dc1..7adc772 100644 --- a/src/test/java/rife/bld/dependencies/TestBom.java +++ b/src/test/java/rife/bld/dependencies/TestBom.java @@ -506,6 +506,48 @@ public class TestBom { } } + @Test + void testDeclaredVersionConflictsAreDetected() throws Exception { + var server = createArtifactServer(Map.of( + "bom1:1.0.0", bomPom("bom1", "1.0.0", managed("a", "1.4.0") + managed("b", "3.0.0"))), + Map.of()); + server.start(); + try { + var retriever = ArtifactRetriever.cachingInstance(); + var repositories = serverRepositories(server); + var scopes = new DependencyScopes(); + scopes.scope(compile) + .include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0))) + // declared at a different version than the BOM manages + .include(new Dependency("com.example", "a", new VersionNumber(2, 2, 0))) + // declared at the same version as the BOM manages + .include(new Dependency("com.example", "b", new VersionNumber(3, 0, 0))); + scopes.scope(Scope.test) + // the same difference through scope composition is + // reported once + .include(new Dependency("com.example", "a", new VersionNumber(2, 2, 0))) + // a version-less dependency takes the BOM version and + // isn't a difference + .include(new Dependency("com.example", "b")); + + var conflicts = scopes.declaredVersionConflicts(new HierarchicalProperties(), retriever, repositories); + assertEquals(1, conflicts.size()); + var conflict = conflicts.get(0); + assertEquals("com.example:a", conflict.dependency()); + assertEquals(Version.parse("2.2.0"), conflict.declaredVersion()); + assertEquals("com.example:bom1", conflict.bom()); + assertEquals(Version.parse("1.4.0"), conflict.bomVersion()); + + // a bld.override that supplies the version prevents the report + var override_properties = new HierarchicalProperties(); + override_properties.put(VersionResolution.PROPERTY_OVERRIDE_PREFIX, "com.example:a:2.2.0"); + assertEquals(List.of(), + scopes.declaredVersionConflicts(override_properties, retriever, repositories)); + } finally { + server.stop(0); + } + } + @Test void testEffectiveBomsComposition() { var compile_bom = new Bom("com.example", "compile-bom", new VersionNumber(1, 0, 0));