From f47afb5b490f3f0a90912a2868e21c8d0e0f1b40 Mon Sep 17 00:00:00 2001 From: PandaMan Date: Mon, 9 Feb 2026 02:27:28 -0500 Subject: [PATCH] [2.x] fix: Fixes evicted warning for version intervals (#8719) When a dependency declares a version range (e.g. Ivy [1.3.1,2.3] or comma-separated "1.3.1,2.3" as used by Coursier), and the resolver picks a version inside that range, sbt was still reporting an eviction warning. The chosen version satisfies the range, so it should not be reported as an eviction. Example (from #6244): oauth2-oidc-sdk and nimbus-jose-jwt both depend on net.minidev:json-smart with range [1.3.1,2.3]. Resolution selects 2.3, which is within the range. Before this fix, sbt reported an eviction for json-smart even though 2.3 satisfies [1.3.1,2.3]. --- .../librarymanagement/VersionRange.scala | 13 ++++++++++- .../librarymanagement/EvictionWarning.scala | 7 ++++-- .../librarymanagement/VersionRangeSpec.scala | 15 +++++++++++++ .../EvictionWarningSpec.scala | 22 +++++++++++++++++++ 4 files changed, 54 insertions(+), 3 deletions(-) diff --git a/lm-core/src/main/scala/sbt/internal/librarymanagement/VersionRange.scala b/lm-core/src/main/scala/sbt/internal/librarymanagement/VersionRange.scala index 055f835aa..bf5c62bba 100644 --- a/lm-core/src/main/scala/sbt/internal/librarymanagement/VersionRange.scala +++ b/lm-core/src/main/scala/sbt/internal/librarymanagement/VersionRange.scala @@ -12,7 +12,9 @@ object VersionRange { (revision.contains("[")) || (revision.contains("]")) || (revision.contains("(")) || - (revision.contains(")")) + (revision.contains(")")) || + // Comma-separated range e.g. "1.3.1,2.3" (fixes #6244 when Coursier passes range without brackets) + (revision.contains(",") && revision.exists(_.isDigit)) } /** @@ -25,6 +27,15 @@ object VersionRange { if (!isVersionRange(range)) { // Not a range, just compare directly version == range + } else if (range.contains(",") && !hasMavenVersionRange(range)) { + // Comma-separated range without brackets e.g. "1.3.1,2.3" (fixes #6244) + val parts = range.split(",", 2) + if (parts.length == 2) { + val lower = parts(0).trim + val upper = parts(1).trim + lower.nonEmpty && upper.nonEmpty && + compareVersions(version, lower) >= 0 && compareVersions(version, upper) <= 0 + } else false } else if (range.endsWith("+")) { // Handle plus ranges like "1.0+" meaning >= 1.0 val base = range.dropRight(1) diff --git a/lm-core/src/main/scala/sbt/librarymanagement/EvictionWarning.scala b/lm-core/src/main/scala/sbt/librarymanagement/EvictionWarning.scala index 42783e630..87c1c27ff 100644 --- a/lm-core/src/main/scala/sbt/librarymanagement/EvictionWarning.scala +++ b/lm-core/src/main/scala/sbt/librarymanagement/EvictionWarning.scala @@ -337,15 +337,18 @@ object EvictionWarning { def guessCompatible(p: EvictionPair): Boolean = p.evicteds forall { r => val winnerOpt = p.winner map { _.module } + val evictedRev = r.module.revision + // Same version: no eviction (fixes #6244 when resolution picks version within requested range) + val sameVersion: Boolean = winnerOpt.exists(_.revision == evictedRev) // Check if the evicted module's revision is a version range and if the winner satisfies it // This handles cases like [4.1.0,5) where 4.2.1 would be within range (fixes #3978) - val evictedRev = r.module.revision + // and [1.3.1,2.3] where 2.3 is valid (fixes #6244) val winnerSatisfiesRange: Boolean = winnerOpt match { case Some(winner) if VersionRange.isVersionRange(evictedRev) => VersionRange.versionSatisfiesRange(winner.revision, evictedRev) case _ => false } - if (winnerSatisfiesRange) { + if (sameVersion || winnerSatisfiesRange) { true } else { val extraAttributes = ((p.winner match { diff --git a/lm-core/src/test/scala/sbt/librarymanagement/VersionRangeSpec.scala b/lm-core/src/test/scala/sbt/librarymanagement/VersionRangeSpec.scala index 08225465b..faba93d46 100644 --- a/lm-core/src/test/scala/sbt/librarymanagement/VersionRangeSpec.scala +++ b/lm-core/src/test/scala/sbt/librarymanagement/VersionRangeSpec.scala @@ -98,4 +98,19 @@ class VersionRangeSpec extends UnitSpec { assert(VersionRange.versionSatisfiesRange("1.0.0", "[1.0]") == true) assert(VersionRange.versionSatisfiesRange("1.1", "[1.0]") == false) } + + // Exact reproduction case from issue #6244 (net.minidev:json-smart, [1.3.1,2.3] -> 2.3 selected) + it should "not treat 2.3 as evicted when range is [1.3.1,2.3] (fixes #6244)" in { + assert(VersionRange.isVersionRange("[1.3.1,2.3]") == true) + assert(VersionRange.versionSatisfiesRange("2.3", "[1.3.1,2.3]") == true) + assert(VersionRange.versionSatisfiesRange("1.3.1", "[1.3.1,2.3]") == true) + assert(VersionRange.versionSatisfiesRange("2.4", "[1.3.1,2.3]") == false) + } + + it should "handle comma-separated range without brackets (fixes #6244)" in { + assert(VersionRange.isVersionRange("1.3.1,2.3") == true) + assert(VersionRange.versionSatisfiesRange("2.3", "1.3.1,2.3") == true) + assert(VersionRange.versionSatisfiesRange("1.3.1", "1.3.1,2.3") == true) + assert(VersionRange.versionSatisfiesRange("2.4", "1.3.1,2.3") == false) + } } diff --git a/lm-ivy/src/test/scala/sbt/internal/librarymanagement/EvictionWarningSpec.scala b/lm-ivy/src/test/scala/sbt/internal/librarymanagement/EvictionWarningSpec.scala index ac4585279..23a10b7aa 100644 --- a/lm-ivy/src/test/scala/sbt/internal/librarymanagement/EvictionWarningSpec.scala +++ b/lm-ivy/src/test/scala/sbt/internal/librarymanagement/EvictionWarningSpec.scala @@ -364,6 +364,28 @@ object EvictionWarningSpec extends BaseIvySpecification { def javaLibDirectDeps = Vector(commonsIo14, commonsIo24) def javaLibTransitiveDeps = Vector(unfilteredUploads080, bnfparser10) def scalaLibTransitiveDeps = Vector(scala2104, bananaSesame04, akkaRemote234) + + // #6244: oauth2-oidc-sdk and nimbus-jose-jwt both depend on net.minidev:json-smart [1.3.1,2.3]; + // resolution selects 2.3, which is within the range, so we must not report it as eviction. + def oauth2OidcSdk822 = + ModuleID("com.nimbusds", "oauth2-oidc-sdk", "8.22").withConfigurations(Some("compile")) + def nimbusJoseJwt8201 = + ModuleID("com.nimbusds", "nimbus-jose-jwt", "8.20.1").withConfigurations(Some("compile")) + def versionIntervalDeps6244 = Vector(oauth2OidcSdk822, nimbusJoseJwt8201) + + test( + "When winner satisfies version range [1.3.1,2.3], should not report eviction for json-smart (fixes #6244)" + ) { + val m = module(defaultModuleId, versionIntervalDeps6244, Some("2.12.12")) + val report = ivyUpdate(m) + val warning = EvictionWarning(m, fullOptions, report) + val jsonSmartEviction = + warning.reportedEvictions.find(p => p.organization == "net.minidev" && p.name == "json-smart") + assert( + jsonSmartEviction.isEmpty, + s"#6244: json-smart 2.3 is within range [1.3.1,2.3], should not be reported as eviction. Got:\n${warning.lines.mkString("\n")}" + ) + } def dummyScalaModuleInfo(v: String): ScalaModuleInfo = ScalaModuleInfo( scalaFullVersion = v,