From 32816b2bdbc5f5b39dade7e12682a463890dc73f Mon Sep 17 00:00:00 2001 From: Alex Archambault Date: Mon, 20 Jan 2025 16:06:46 +0100 Subject: [PATCH 1/6] Keep parsed string in VersionConstraint, parse it lazily --- build.sc | 2 +- .../version/ConstraintReconciliation.scala | 9 +- .../shared/src/coursier/version/Version.scala | 15 +- .../coursier/version/VersionConstraint.scala | 170 +++++++++++++----- .../coursier/version/VersionInterval.scala | 6 +- .../src/coursier/version/VersionParse.scala | 8 +- .../version/VersionConstraintTests.scala | 33 ++-- .../version/VersionIntervalTests.scala | 7 +- 8 files changed, 168 insertions(+), 82 deletions(-) diff --git a/build.sc b/build.sc index 1881045..9715b91 100644 --- a/build.sc +++ b/build.sc @@ -106,7 +106,7 @@ trait Versions extends Cross.Module[String] with ScalaModule with VersionsPublis } def compileIvyDeps = Agg( - ivy"io.github.alexarchambault::data-class:0.2.6" + ivy"io.github.alexarchambault::data-class:0.2.7" ) def mimaBinaryIssueFilters = super.mimaBinaryIssueFilters() ++ Seq( diff --git a/versions/shared/src/coursier/version/ConstraintReconciliation.scala b/versions/shared/src/coursier/version/ConstraintReconciliation.scala index cf0a01c..3f6ad08 100644 --- a/versions/shared/src/coursier/version/ConstraintReconciliation.scala +++ b/versions/shared/src/coursier/version/ConstraintReconciliation.scala @@ -61,7 +61,7 @@ object ConstraintReconciliation { else { val parsedConstraints = standard.map(VersionParse.versionConstraint) VersionConstraint.merge(parsedConstraints: _*) - .flatMap(_.repr) + .map(_.asString) } val retainedLatestOpt = retainLatestOpt(latests) @@ -78,7 +78,7 @@ object ConstraintReconciliation { retainedLatestOpt else VersionConstraint.merge(parsedIntervals: _*) - .flatMap(_.repr) + .map(_.asString) .map(itv => (itv +: retainedLatestOpt.toSeq).mkString("&")) } } @@ -102,9 +102,10 @@ object ConstraintReconciliation { else if (standard.lengthCompare(1) == 0) standard.headOption else { val parsedConstraints = standard.map(VersionParse.versionConstraint) - VersionConstraint.merge(parsedConstraints: _*) + val repr = VersionConstraint.merge(parsedConstraints: _*) .getOrElse(VersionConstraint.relaxedMerge(parsedConstraints: _*)) - .repr + .asString + Some(repr) } val retainedLatestOpt = retainLatestOpt(latests) if (latests.isEmpty) diff --git a/versions/shared/src/coursier/version/Version.scala b/versions/shared/src/coursier/version/Version.scala index 541d292..b525d0a 100644 --- a/versions/shared/src/coursier/version/Version.scala +++ b/versions/shared/src/coursier/version/Version.scala @@ -11,7 +11,14 @@ import scala.annotation.tailrec * Same kind of ordering as aether-util/src/main/java/org/eclipse/aether/util/version/GenericVersion.java */ @data class Version(repr: String) extends Ordered[Version] { - lazy val items: Vector[Version.Item] = Version.items(repr) + def asString: String = repr + private var items0: Vector[Version.Item] = null + def items: Vector[Version.Item] = { + // no need to guard against concurrent computations, this is not too expensive to compute + if (items0 == null) + items0 = Version.items(repr) + items0 + } def compare(other: Version) = Version.listCompare(items, other.items) def isEmpty = items.forall(_.isEmpty) @@ -21,11 +28,15 @@ import scala.annotation.tailrec repr .split(Array('.', '-')) .forall(_.lengthCompare(5) <= 0) + + override lazy val hashCode = repr.hashCode() } object Version { - private[version] val zero = Version("0") + private val zero0 = Version("0") + + def zero: Version = Version("0") sealed abstract class Item extends Ordered[Item] { def compare(other: Item): Int = diff --git a/versions/shared/src/coursier/version/VersionConstraint.scala b/versions/shared/src/coursier/version/VersionConstraint.scala index 465a3c9..d047a39 100644 --- a/versions/shared/src/coursier/version/VersionConstraint.scala +++ b/versions/shared/src/coursier/version/VersionConstraint.scala @@ -4,10 +4,18 @@ import dataclass.data import scala.annotation.tailrec -@data class VersionConstraint( - interval: VersionInterval, - preferred: Seq[Version] -) { +sealed abstract class VersionConstraint extends Product with Serializable with Ordered[VersionConstraint] { + def asString: String + def interval: VersionInterval + def preferred: Seq[Version] + + def generateString: String = + VersionConstraint.generateString(interval, preferred) + + private lazy val compareKey = preferred.headOption.orElse(interval.from).getOrElse(Version.zero) + def compare(other: VersionConstraint): Int = + compareKey.compare(other.compareKey) + def isValid: Boolean = interval.isValid && preferred.forall { v => interval.contains(v) || @@ -16,56 +24,71 @@ import scala.annotation.tailrec cmp < 0 || (cmp == 0 && interval.toIncluded) } } - - def blend: Option[Either[VersionInterval, Version]] = - if (isValid) { - val preferredInInterval = preferred.filter(interval.contains) - - if (preferredInInterval.isEmpty) - Some(Left(interval)) - else - Some(Right(preferredInInterval.max)) - } else - None - - def repr: Option[String] = - blend.map { - case Left(itv) => - if (itv == VersionInterval.zero) - "" - else - itv.repr - case Right(v) => v.repr - } } object VersionConstraint { + def apply(version: String): VersionConstraint = + Lazy(version) + def from(interval: VersionInterval, preferred: Seq[Version]): VersionConstraint = + VersionConstraint.Eager( + generateString(interval, preferred), + interval, + preferred + ) + + def empty: VersionConstraint = + empty0 + + private def generateString(interval: VersionInterval, preferred: Seq[Version]): String = + if (interval == VersionInterval.zero && preferred.isEmpty) + "" + else if (interval == VersionInterval.zero && preferred.length == 1) + preferred.head.repr + else if (preferred.isEmpty) + interval.repr + else if (interval == VersionInterval.zero) + preferred.map(_.repr).mkString(";") + else + interval.repr + "&" + preferred.map(_.repr).mkString(";") + // sys.error("TODO / string representation of interval and preferred versions together") + + def fromVersion(version: Version): VersionConstraint = + Eager( + version.repr, + VersionInterval.zero, + Seq(version) + ) + + def merge(constraints: VersionConstraint*): Option[VersionConstraint] = + if (constraints.isEmpty) Some(empty) + else if (constraints.length == 1) Some(constraints.head).filter(_.isValid) + else { + val intervals = constraints.map(_.interval) + + val intervalOpt = + intervals.foldLeft(Option(VersionInterval.zero)) { + case (acc, itv) => + acc.flatMap(_.merge(itv)) + } - def preferred(version: Version): VersionConstraint = - VersionConstraint(VersionInterval.zero, Seq(version)) - def interval(interval: VersionInterval): VersionConstraint = - VersionConstraint(interval, Nil) - - val all = VersionConstraint(VersionInterval.zero, Nil) - - def merge(constraints: VersionConstraint*): Option[VersionConstraint] = { - - val intervals = constraints.map(_.interval) - - val intervalOpt = - intervals.foldLeft(Option(VersionInterval.zero)) { - case (acc, itv) => - acc.flatMap(_.merge(itv)) + val constraintOpt = intervalOpt.map { interval => + val preferreds = constraints.flatMap(_.preferred).distinct + val repr = + if (interval == VersionInterval.zero && preferreds.length == 1) + preferreds.head.repr + else if (preferreds.isEmpty) + interval.repr + else if (interval == VersionInterval.zero) + preferreds.map(_.repr).mkString(";") + else + interval.repr + "&" + preferreds.map(_.repr).mkString(";") + // sys.error("TODO / string representation of interval and preferred versions together") + VersionConstraint.Eager(repr, interval, preferreds) } - val constraintOpt = intervalOpt.map { interval => - val preferreds = constraints.flatMap(_.preferred).distinct - VersionConstraint(interval, preferreds) + constraintOpt.filter(_.isValid) } - constraintOpt.filter(_.isValid) - } - // 1. sort constraints in ascending order. // 2. from the right, merge them two-by-two with the merge method above // 3. return the last successful merge @@ -84,7 +107,7 @@ object VersionConstraint { val cs = constraints.toList cs match { - case Nil => VersionConstraint.all + case Nil => VersionConstraint.empty case h :: Nil => h case _ => val sorted = cs.sortBy { c => @@ -96,4 +119,57 @@ object VersionConstraint { mergeByTwo(reversed.head, reversed.tail) } } + + + private[version] def fromPreferred(input: String, version: Version): VersionConstraint = + Eager(input, VersionInterval.zero, Seq(version)) + private[version] def fromInterval(input: String, interval: VersionInterval): VersionConstraint = + Eager(input, interval, Nil) + + private val empty0 = Eager("", VersionInterval.zero, Nil) + + private[coursier] val parsedValueAsToString: ThreadLocal[Boolean] = new ThreadLocal[Boolean] { + override protected def initialValue(): Boolean = + false + } + + @data class Lazy(asString: String) extends VersionConstraint { + private var parsed0: VersionConstraint = null + private def parsed = { + if (parsed0 == null) + parsed0 = VersionParse.versionConstraint(asString) + parsed0 + } + def interval: VersionInterval = parsed.interval + def preferred: Seq[Version] = parsed.preferred + + override def toString: String = + if (parsedValueAsToString.get()) asString + else + s"VersionConstraint.Lazy($asString, ${if (parsed0 == null) "[unparsed]" else s"$interval, $preferred"})" + override def hashCode(): Int = + (VersionConstraint, asString).hashCode() + override def equals(obj: Any): Boolean = + obj.isInstanceOf[VersionConstraint] && { + val other = obj.asInstanceOf[VersionConstraint] + asString == other.asString + } + } + @data class Eager( + asString: String, + interval: VersionInterval, + preferred: Seq[Version] + ) extends VersionConstraint { + override def toString: String = + if (parsedValueAsToString.get()) asString + else + s"VersionConstraint.Eager($asString, $interval, $preferred)" + override def hashCode(): Int = + (VersionConstraint, asString).hashCode() + override def equals(obj: Any): Boolean = + obj.isInstanceOf[VersionConstraint] && { + val other = obj.asInstanceOf[VersionConstraint] + asString == other.asString + } + } } diff --git a/versions/shared/src/coursier/version/VersionInterval.scala b/versions/shared/src/coursier/version/VersionInterval.scala index 033bf4f..ffc1bd9 100644 --- a/versions/shared/src/coursier/version/VersionInterval.scala +++ b/versions/shared/src/coursier/version/VersionInterval.scala @@ -68,13 +68,13 @@ import dataclass.data def constraint: VersionConstraint = this match { - case VersionInterval.zero => VersionConstraint.all + case VersionInterval.zero => VersionConstraint.empty case itv => (itv.from, itv.to, itv.fromIncluded, itv.toIncluded) match { case (Some(version), None, true, false) => - VersionConstraint.preferred(version) + VersionConstraint.fromPreferred(version.repr, version) case _ => - VersionConstraint.interval(itv) + VersionConstraint.fromInterval(repr, itv) } } diff --git a/versions/shared/src/coursier/version/VersionParse.scala b/versions/shared/src/coursier/version/VersionParse.scala index f61450f..4d5eb6a 100644 --- a/versions/shared/src/coursier/version/VersionParse.scala +++ b/versions/shared/src/coursier/version/VersionParse.scala @@ -75,12 +75,12 @@ object VersionParse { } def versionConstraint(s: String): VersionConstraint = { - def noConstraint = if (s.isEmpty) Some(VersionConstraint.all) else None + def noConstraint = if (s.isEmpty) Some(VersionConstraint.empty) else None noConstraint - .orElse(ivyLatestSubRevisionInterval(s).map(VersionConstraint.interval)) - .orElse(versionInterval(s).orElse(multiVersionInterval(s)).map(VersionConstraint.interval)) - .getOrElse(VersionConstraint.preferred(Version(s))) + .orElse(ivyLatestSubRevisionInterval(s).map(VersionConstraint.fromInterval(s, _))) + .orElse(versionInterval(s).orElse(multiVersionInterval(s)).map(VersionConstraint.fromInterval(s, _))) + .getOrElse(VersionConstraint.fromPreferred(s, Version(s))) } } diff --git a/versions/shared/test/src/coursier/version/VersionConstraintTests.scala b/versions/shared/test/src/coursier/version/VersionConstraintTests.scala index 2417d34..5d08bed 100644 --- a/versions/shared/test/src/coursier/version/VersionConstraintTests.scala +++ b/versions/shared/test/src/coursier/version/VersionConstraintTests.scala @@ -1,6 +1,5 @@ package coursier.version -import coursier.version.{Version, VersionConstraint, VersionInterval, VersionParse} import utest._ object VersionConstraintTests extends TestSuite { @@ -9,27 +8,27 @@ object VersionConstraintTests extends TestSuite { "parse" - { "empty" - { val c0 = VersionParse.versionConstraint("") - assert(c0 == VersionConstraint.all) + assert(c0 == VersionConstraint.empty) } "basicVersion" - { val c0 = VersionParse.versionConstraint("1.2") - assert(c0 == VersionConstraint.preferred(Version("1.2"))) + assert(c0 == VersionConstraint.fromPreferred("1.2", Version("1.2"))) } "basicVersionInterval" - { val c0 = VersionParse.versionConstraint("(,1.2]") - assert(c0 == VersionConstraint.interval(VersionInterval(None, Some(Version("1.2")), false, true))) + assert(c0 == VersionConstraint.fromInterval("(,1.2]", VersionInterval(None, Some(Version("1.2")), false, true))) } "latestSubRevision" - { val c0 = VersionParse.versionConstraint("1.2.3-+") - assert(c0 == VersionConstraint.interval(VersionInterval(Some(Version("1.2.3")), Some(Version("1.2.3-max")), true, true))) + assert(c0 == VersionConstraint.fromInterval("1.2.3-+", VersionInterval(Some(Version("1.2.3")), Some(Version("1.2.3-max")), true, true))) } "latestSubRevisionWithLiteral" - { val c0 = VersionParse.versionConstraint("1.2.3-rc-+") - assert(c0 == VersionConstraint.interval(VersionInterval(Some(Version("1.2.3-rc")), Some(Version("1.2.3-rc-max")), true, true))) + assert(c0 == VersionConstraint.fromInterval("1.2.3-rc-+", VersionInterval(Some(Version("1.2.3-rc")), Some(Version("1.2.3-rc-max")), true, true))) } "latestSubRevisionWithZero" - { val c0 = VersionParse.versionConstraint("1.0.+") - assert(c0 == VersionConstraint.interval(VersionInterval(Some(Version("1.0")), Some(Version("1.0.max")), true, true))) + assert(c0 == VersionConstraint.fromInterval("1.0.+", VersionInterval(Some(Version("1.0")), Some(Version("1.0.max")), true, true))) } } @@ -54,16 +53,16 @@ object VersionConstraintTests extends TestSuite { "repr" - { "empty" - { - val s0 = VersionConstraint.all.repr - assert(s0.contains("")) + val s0 = VersionConstraint.empty.generateString + assert(s0 == "") } "preferred" - { - val s0 = VersionConstraint.preferred(Version("2.1")).repr - assert(s0.contains("2.1")) + val s0 = VersionConstraint.fromPreferred("2.1", Version("2.1")).generateString + assert(s0 == "2.1") } "interval" - { - val s0 = VersionConstraint.interval(VersionInterval(None, Some(Version("2.1")), false, true)).repr - assert(s0.contains("(,2.1]")) + val s0 = VersionConstraint.fromInterval("(,2.1]", VersionInterval(None, Some(Version("2.1")), false, true)).generateString + assert(s0 == "(,2.1]") } } @@ -71,8 +70,8 @@ object VersionConstraintTests extends TestSuite { * - { val s0 = VersionConstraint.merge( VersionParse.versionConstraint("[1.0,3.2]"), - VersionParse.versionConstraint("[3.0,4.0)")).get.repr - assert(s0.contains("[3.0,3.2]")) + VersionParse.versionConstraint("[3.0,4.0)")).get.generateString + assert(s0 == "[3.0,3.2]") } * - { @@ -95,8 +94,8 @@ object VersionConstraintTests extends TestSuite { * - { val s0 = VersionConstraint.relaxedMerge( VersionParse.versionConstraint("[1.0,2.0)"), - VersionParse.versionConstraint("[3.0,4.0)")).repr - assert(s0 == Some("[3.0,4.0)")) + VersionParse.versionConstraint("[3.0,4.0)")).generateString + assert(s0 == "[3.0,4.0)") } * - { diff --git a/versions/shared/test/src/coursier/version/VersionIntervalTests.scala b/versions/shared/test/src/coursier/version/VersionIntervalTests.scala index 3a461d8..19bdcda 100644 --- a/versions/shared/test/src/coursier/version/VersionIntervalTests.scala +++ b/versions/shared/test/src/coursier/version/VersionIntervalTests.scala @@ -1,6 +1,5 @@ package coursier.version -import coursier.version.{Version, VersionConstraint, VersionInterval, VersionParse} import utest._ object VersionIntervalTests extends TestSuite { @@ -322,17 +321,17 @@ object VersionIntervalTests extends TestSuite { "none" - { val s1 = "(,)" val c1 = VersionParse.versionInterval(s1).map(_.constraint) - assert(c1 == Some(VersionConstraint.all)) + assert(c1 == Some(VersionConstraint.empty)) } "preferred" - { val s1 = "[1.3,)" val c1 = VersionParse.versionInterval(s1).map(_.constraint) - assert(c1 == Some(VersionConstraint.preferred(VersionParse.version("1.3").get))) + assert(c1 == Some(VersionConstraint.fromPreferred("1.3", VersionParse.version("1.3").get))) } "interval" - { val s1 = "[1.3,2.4)" val c1 = VersionParse.versionInterval(s1).map(_.constraint) - assert(c1 == Some(VersionConstraint.interval(VersionInterval(VersionParse.version("1.3"), VersionParse.version("2.4"), true, false)))) + assert(c1 == Some(VersionConstraint.fromInterval(s1, VersionInterval(VersionParse.version("1.3"), VersionParse.version("2.4"), true, false)))) } } } From 2246c2f90d175b29528c66f76c18e85cc9464cb9 Mon Sep 17 00:00:00 2001 From: Alex Archambault Date: Mon, 20 Jan 2025 16:25:08 +0100 Subject: [PATCH 2/6] Reset MiMA --- build.sc | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/build.sc b/build.sc index 9715b91..2f016f9 100644 --- a/build.sc +++ b/build.sc @@ -28,6 +28,7 @@ trait VersionsMima extends Mima { val current = os.proc("git", "describe", "--tags", "--match", "v*") .call(cwd = T.workspace) .out.trim() + val cutOff = coursier.core.Version("0.3.3") os.proc("git", "tag", "-l") .call(cwd = T.workspace) .out.lines() @@ -35,9 +36,21 @@ trait VersionsMima extends Mima { .filter(_.startsWith("v")) .map(_.stripPrefix("v")) .map(coursier.core.Version(_)) + .filter(_ > cutOff) .sorted .map(_.repr) } + // required if mimaPreviousVersions is empty + def mimaPreviousArtifacts = T { + val versions = mimaPreviousVersions().distinct + mill.api.Result.Success( + Agg.from( + versions.map(version => + ivy"${pomSettings().organization}:${artifactId()}:$version" + ) + ) + ) + } } trait VersionsPublishModule extends PublishModule with VersionsMima { From 683174ca0ba439f73ff5015ab1c21c39c7d1fb0e Mon Sep 17 00:00:00 2001 From: Alex Archambault Date: Mon, 20 Jan 2025 16:29:06 +0100 Subject: [PATCH 3/6] Address compilation warning --- versions/shared/src/coursier/version/ModuleMatcher.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/versions/shared/src/coursier/version/ModuleMatcher.scala b/versions/shared/src/coursier/version/ModuleMatcher.scala index 1866576..87bb1f0 100644 --- a/versions/shared/src/coursier/version/ModuleMatcher.scala +++ b/versions/shared/src/coursier/version/ModuleMatcher.scala @@ -20,7 +20,11 @@ import scala.util.matching.Regex lazy val orgPattern = blobToPattern(organizationMatcher) lazy val namePattern = blobToPattern(nameMatcher) lazy val attributesPattern = attributeMatchers - .mapValues(blobToPattern(_)) + .iterator + .map { + case (k, v) => + (k, blobToPattern(v)) + } .toMap def matches(organization: String, name: String): Boolean = From d95f64abd2f4aa04882b2239b251cdf8bbca15dc Mon Sep 17 00:00:00 2001 From: Alex Archambault Date: Mon, 20 Jan 2025 19:31:42 +0100 Subject: [PATCH 4/6] Stop parsing things in ConstraintReconciliation --- .../version/ConstraintReconciliation.scala | 51 +++++++------------ 1 file changed, 19 insertions(+), 32 deletions(-) diff --git a/versions/shared/src/coursier/version/ConstraintReconciliation.scala b/versions/shared/src/coursier/version/ConstraintReconciliation.scala index 3f6ad08..a0c840f 100644 --- a/versions/shared/src/coursier/version/ConstraintReconciliation.scala +++ b/versions/shared/src/coursier/version/ConstraintReconciliation.scala @@ -6,16 +6,16 @@ package coursier.version * To be used mainly during resolution. */ sealed abstract class ConstraintReconciliation extends Product with Serializable { - def reconcile(versions: Seq[String]): Option[String] + def reconcile(versions: Seq[VersionConstraint]): Option[VersionConstraint] } object ConstraintReconciliation { - private final val LatestIntegration = "latest.integration" - private final val LatestRelease = "latest.release" - private final val LatestStable = "latest.stable" + private final val LatestIntegration = VersionConstraint("latest.integration") + private final val LatestRelease = VersionConstraint("latest.release") + private final val LatestStable = VersionConstraint("latest.stable") - private def splitStandard(versions: Seq[String]): (Seq[String], Seq[String]) = + private def splitStandard(versions: Seq[VersionConstraint]): (Seq[VersionConstraint], Seq[VersionConstraint]) = versions.distinct.partition { case LatestIntegration => false case LatestRelease => false @@ -23,16 +23,14 @@ object ConstraintReconciliation { case _ => true } - private def retainLatestOpt(latests: Seq[String]): Option[String] = + private def retainLatestOpt(latests: Seq[VersionConstraint]): Option[VersionConstraint] = if (latests.isEmpty) None else if (latests.lengthCompare(1) == 0) latests.headOption else { val set = latests.toSet val retained = - if (set(LatestIntegration)) - LatestIntegration - else if (set(LatestRelease)) - LatestRelease + if (set(LatestIntegration)) LatestIntegration + else if (set(LatestRelease)) LatestRelease else { // at least two distinct latest.* means we shouldn't even reach this else block anyway assert(set(LatestStable)) @@ -48,7 +46,7 @@ object ConstraintReconciliation { * Fails when passed version intervals that don't overlap. */ case object Default extends ConstraintReconciliation { - def reconcile(versions: Seq[String]): Option[String] = + def reconcile(versions: Seq[VersionConstraint]): Option[VersionConstraint] = if (versions.isEmpty) None else if (versions.lengthCompare(1) == 0) @@ -58,19 +56,14 @@ object ConstraintReconciliation { val retainedStandard = if (standard.isEmpty) None else if (standard.lengthCompare(1) == 0) standard.headOption - else { - val parsedConstraints = standard.map(VersionParse.versionConstraint) - VersionConstraint.merge(parsedConstraints: _*) - .map(_.asString) - } + else + VersionConstraint.merge(standard: _*) val retainedLatestOpt = retainLatestOpt(latests) - if (standard.isEmpty) - retainedLatestOpt - else if (latests.isEmpty) - retainedStandard + if (standard.isEmpty) retainedLatestOpt + else if (latests.isEmpty) retainedStandard else { - val parsedIntervals = standard.map(VersionParse.versionConstraint) + val parsedIntervals = standard .filter(_.preferred.isEmpty) // only keep intervals .filter(_.interval != VersionInterval.zero) // not interval matching any version @@ -78,8 +71,6 @@ object ConstraintReconciliation { retainedLatestOpt else VersionConstraint.merge(parsedIntervals: _*) - .map(_.asString) - .map(itv => (itv +: retainedLatestOpt.toSeq).mkString("&")) } } } @@ -90,7 +81,7 @@ object ConstraintReconciliation { * When passed version intervals that don't overlap, the lowest intervals are discarded until the remaining intervals do overlap. */ case object Relaxed extends ConstraintReconciliation { - def reconcile(versions: Seq[String]): Option[String] = + def reconcile(versions: Seq[VersionConstraint]): Option[VersionConstraint] = if (versions.isEmpty) None else if (versions.lengthCompare(1) == 0) @@ -101,17 +92,13 @@ object ConstraintReconciliation { if (standard.isEmpty) None else if (standard.lengthCompare(1) == 0) standard.headOption else { - val parsedConstraints = standard.map(VersionParse.versionConstraint) - val repr = VersionConstraint.merge(parsedConstraints: _*) - .getOrElse(VersionConstraint.relaxedMerge(parsedConstraints: _*)) - .asString + val repr = VersionConstraint.merge(standard: _*) + .getOrElse(VersionConstraint.relaxedMerge(standard: _*)) Some(repr) } val retainedLatestOpt = retainLatestOpt(latests) - if (latests.isEmpty) - retainedStandard - else - retainedLatestOpt + if (latests.isEmpty) retainedStandard + else retainedLatestOpt } } From ed5fae301dd4adbcb057e82198537efd9406e3ee Mon Sep 17 00:00:00 2001 From: Alex Archambault Date: Mon, 20 Jan 2025 20:52:17 +0100 Subject: [PATCH 5/6] Report latest reconciliation stuff from coursier --- .../version/ConstraintReconciliation.scala | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/versions/shared/src/coursier/version/ConstraintReconciliation.scala b/versions/shared/src/coursier/version/ConstraintReconciliation.scala index a0c840f..15acc67 100644 --- a/versions/shared/src/coursier/version/ConstraintReconciliation.scala +++ b/versions/shared/src/coursier/version/ConstraintReconciliation.scala @@ -117,4 +117,36 @@ object ConstraintReconciliation { case _ => Default } + /** Strict version reconciliation. + * + * This particular instance behaves the same as [[Default]] when used by + * [[coursier.core.Resolution]]. Actual strict conflict manager is handled by + * `coursier.params.rule.Strict`, which is set up by `coursier.Resolve` when a strict + * reconciliation is added to it. + */ + case object Strict extends ConstraintReconciliation { + def reconcile(versions: Seq[VersionConstraint]): Option[VersionConstraint] = + Default.reconcile(versions) + } + + /** Semantic versioning version reconciliation. + * + * This particular instance behaves the same as [[Default]] when used by + * [[coursier.core.Resolution]]. Actual semantic versioning checks are handled by + * `coursier.params.rule.Strict` with field `semVer = true`, which is set up by + * `coursier.Resolve` when a SemVer reconciliation is added to it. + */ + case object SemVer extends ConstraintReconciliation { + def reconcile(versions: Seq[VersionConstraint]): Option[VersionConstraint] = + Default.reconcile(versions) + } + + def apply(input: String): Option[ConstraintReconciliation] = + input match { + case "default" => Some(Default) + case "relaxed" => Some(Relaxed) + case "strict" => Some(Strict) + case "semver" => Some(SemVer) + case _ => None + } } From a69a46aa49a72b3dbdee8e4b91756af02f6421bb Mon Sep 17 00:00:00 2001 From: Alex Archambault Date: Mon, 20 Jan 2025 20:52:46 +0100 Subject: [PATCH 6/6] Add helpers to normalize VersionConstraint-s --- .../version/ConstraintReconciliation.scala | 4 ++ .../coursier/version/VersionConstraint.scala | 52 +++++++++++++------ 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/versions/shared/src/coursier/version/ConstraintReconciliation.scala b/versions/shared/src/coursier/version/ConstraintReconciliation.scala index 15acc67..23c0c4b 100644 --- a/versions/shared/src/coursier/version/ConstraintReconciliation.scala +++ b/versions/shared/src/coursier/version/ConstraintReconciliation.scala @@ -58,6 +58,7 @@ object ConstraintReconciliation { else if (standard.lengthCompare(1) == 0) standard.headOption else VersionConstraint.merge(standard: _*) + .map(_.uniquePreferred.removeUnusedPreferred) val retainedLatestOpt = retainLatestOpt(latests) if (standard.isEmpty) retainedLatestOpt @@ -71,6 +72,7 @@ object ConstraintReconciliation { retainedLatestOpt else VersionConstraint.merge(parsedIntervals: _*) + .map(_.uniquePreferred.removeUnusedPreferred) // FIXME Add retainedLatestOpt too } } } @@ -94,6 +96,8 @@ object ConstraintReconciliation { else { val repr = VersionConstraint.merge(standard: _*) .getOrElse(VersionConstraint.relaxedMerge(standard: _*)) + .uniquePreferred + .removeUnusedPreferred Some(repr) } val retainedLatestOpt = retainLatestOpt(latests) diff --git a/versions/shared/src/coursier/version/VersionConstraint.scala b/versions/shared/src/coursier/version/VersionConstraint.scala index d047a39..2bd6241 100644 --- a/versions/shared/src/coursier/version/VersionConstraint.scala +++ b/versions/shared/src/coursier/version/VersionConstraint.scala @@ -7,8 +7,29 @@ import scala.annotation.tailrec sealed abstract class VersionConstraint extends Product with Serializable with Ordered[VersionConstraint] { def asString: String def interval: VersionInterval + + /** + * Preferred versions + * + * Always sorted in reverse order (higher version upfront) + */ def preferred: Seq[Version] + def uniquePreferred: VersionConstraint = + if (preferred.lengthCompare(1) <= 0) this + else + VersionConstraint.from(interval, Seq(preferred.head)) + def removeUnusedPreferred: VersionConstraint = { + val (keep, ignore) = preferred.partition { v => + interval.from.forall { from => + val cmp = v.compare(from) + cmp >= 0 || (interval.fromIncluded && cmp == 0) + } + } + if (ignore.isEmpty) this + else VersionConstraint.from(interval, keep) + } + def generateString: String = VersionConstraint.generateString(interval, preferred) @@ -29,12 +50,23 @@ sealed abstract class VersionConstraint extends Product with Serializable with O object VersionConstraint { def apply(version: String): VersionConstraint = Lazy(version) - def from(interval: VersionInterval, preferred: Seq[Version]): VersionConstraint = - VersionConstraint.Eager( - generateString(interval, preferred), + def from(interval: VersionInterval, preferred: Seq[Version]): VersionConstraint = { + val isSorted = preferred.iterator.sliding(2).withPartial(false).forall { + case Seq(a, b) => + val cmp = a.compare(b) + cmp > 0 || + // FIXME We'd need to disambiguate versions equals per "def compare" but not per "def equals" + (cmp == 0 && a.asString != b.asString) + } + val preferred0 = + if (isSorted) preferred + else preferred.distinct.sorted.reverse + Eager( + generateString(interval, preferred0), interval, - preferred + preferred0 ) + } def empty: VersionConstraint = empty0 @@ -73,17 +105,7 @@ object VersionConstraint { val constraintOpt = intervalOpt.map { interval => val preferreds = constraints.flatMap(_.preferred).distinct - val repr = - if (interval == VersionInterval.zero && preferreds.length == 1) - preferreds.head.repr - else if (preferreds.isEmpty) - interval.repr - else if (interval == VersionInterval.zero) - preferreds.map(_.repr).mkString(";") - else - interval.repr + "&" + preferreds.map(_.repr).mkString(";") - // sys.error("TODO / string representation of interval and preferred versions together") - VersionConstraint.Eager(repr, interval, preferreds) + from(interval, preferreds) } constraintOpt.filter(_.isValid)