Warn when declared versions differ from applicable BOMs and only report in download

This commit is contained in:
Geert Bevin 2026-07-17 17:18:33 -04:00
parent 7d77f48aec
commit ec185788a1
5 changed files with 165 additions and 19 deletions

View file

@ -178,6 +178,40 @@ public class DependencyScopes extends LinkedHashMap<Scope, DependencySet> {
return result;
}
/**
* Returns the declared dependencies whose explicit version differs
* from the version that a BOM applying to their scope manages them at.
* <p>
* 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<VersionResolution.DeclaredVersionConflict> declaredVersionConflicts(HierarchicalProperties properties, ArtifactRetriever retriever, List<Repository> repositories) {
var result = new ArrayList<VersionResolution.DeclaredVersionConflict>();
var seen = new HashSet<String>();
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.
*

View file

@ -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.
* <p>
* 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.
* <p>
* 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<DeclaredVersionConflict> resolveDeclaredVersionConflicts(HierarchicalProperties properties, ArtifactRetriever retriever, List<Repository> repositories, Collection<Bom> boms, Collection<Dependency> declared) {
var base = new VersionResolution(properties);
var managed_versions = new LinkedHashMap<String, Version>();
var managed_boms = new LinkedHashMap<String, String>();
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<DeclaredVersionConflict>();
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

View file

@ -89,6 +89,9 @@ public class DownloadOperation extends AbstractOperation<DownloadOperation> {
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<DownloadOperation> {
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.
*

View file

@ -54,7 +54,6 @@ public class PurgeOperation extends AbstractOperation<PurgeOperation> {
return;
}
executeReportUncoveredDependencies();
executePurgeCompileDependencies();
executePurgeProvidedDependencies();
executePurgeRuntimeDependencies();
@ -65,24 +64,6 @@ public class PurgeOperation extends AbstractOperation<PurgeOperation> {
}
}
/**
* 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.
*

View file

@ -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));