Skip to content

Commit cefb42d

Browse files
Merge pull request #72 from alexarchambault/lazy-constraint
Keep parsed string in VersionConstraint, parse it lazily
2 parents e9e65bd + a69a46a commit cefb42d

File tree

9 files changed

+258
-110
lines changed

9 files changed

+258
-110
lines changed

build.sc

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,29 @@ trait VersionsMima extends Mima {
2828
val current = os.proc("git", "describe", "--tags", "--match", "v*")
2929
.call(cwd = T.workspace)
3030
.out.trim()
31+
val cutOff = coursier.core.Version("0.3.3")
3132
os.proc("git", "tag", "-l")
3233
.call(cwd = T.workspace)
3334
.out.lines()
3435
.filter(_ != current)
3536
.filter(_.startsWith("v"))
3637
.map(_.stripPrefix("v"))
3738
.map(coursier.core.Version(_))
39+
.filter(_ > cutOff)
3840
.sorted
3941
.map(_.repr)
4042
}
43+
// required if mimaPreviousVersions is empty
44+
def mimaPreviousArtifacts = T {
45+
val versions = mimaPreviousVersions().distinct
46+
mill.api.Result.Success(
47+
Agg.from(
48+
versions.map(version =>
49+
ivy"${pomSettings().organization}:${artifactId()}:$version"
50+
)
51+
)
52+
)
53+
}
4154
}
4255

4356
trait VersionsPublishModule extends PublishModule with VersionsMima {
@@ -106,7 +119,7 @@ trait Versions extends Cross.Module[String] with ScalaModule with VersionsPublis
106119
}
107120

108121
def compileIvyDeps = Agg(
109-
ivy"io.github.alexarchambault::data-class:0.2.6"
122+
ivy"io.github.alexarchambault::data-class:0.2.7"
110123
)
111124

112125
def mimaBinaryIssueFilters = super.mimaBinaryIssueFilters() ++ Seq(

versions/shared/src/coursier/version/ConstraintReconciliation.scala

Lines changed: 56 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,31 @@ package coursier.version
66
* To be used mainly during resolution.
77
*/
88
sealed abstract class ConstraintReconciliation extends Product with Serializable {
9-
def reconcile(versions: Seq[String]): Option[String]
9+
def reconcile(versions: Seq[VersionConstraint]): Option[VersionConstraint]
1010
}
1111

1212
object ConstraintReconciliation {
1313

14-
private final val LatestIntegration = "latest.integration"
15-
private final val LatestRelease = "latest.release"
16-
private final val LatestStable = "latest.stable"
14+
private final val LatestIntegration = VersionConstraint("latest.integration")
15+
private final val LatestRelease = VersionConstraint("latest.release")
16+
private final val LatestStable = VersionConstraint("latest.stable")
1717

18-
private def splitStandard(versions: Seq[String]): (Seq[String], Seq[String]) =
18+
private def splitStandard(versions: Seq[VersionConstraint]): (Seq[VersionConstraint], Seq[VersionConstraint]) =
1919
versions.distinct.partition {
2020
case LatestIntegration => false
2121
case LatestRelease => false
2222
case LatestStable => false
2323
case _ => true
2424
}
2525

26-
private def retainLatestOpt(latests: Seq[String]): Option[String] =
26+
private def retainLatestOpt(latests: Seq[VersionConstraint]): Option[VersionConstraint] =
2727
if (latests.isEmpty) None
2828
else if (latests.lengthCompare(1) == 0) latests.headOption
2929
else {
3030
val set = latests.toSet
3131
val retained =
32-
if (set(LatestIntegration))
33-
LatestIntegration
34-
else if (set(LatestRelease))
35-
LatestRelease
32+
if (set(LatestIntegration)) LatestIntegration
33+
else if (set(LatestRelease)) LatestRelease
3634
else {
3735
// at least two distinct latest.* means we shouldn't even reach this else block anyway
3836
assert(set(LatestStable))
@@ -48,7 +46,7 @@ object ConstraintReconciliation {
4846
* Fails when passed version intervals that don't overlap.
4947
*/
5048
case object Default extends ConstraintReconciliation {
51-
def reconcile(versions: Seq[String]): Option[String] =
49+
def reconcile(versions: Seq[VersionConstraint]): Option[VersionConstraint] =
5250
if (versions.isEmpty)
5351
None
5452
else if (versions.lengthCompare(1) == 0)
@@ -58,28 +56,23 @@ object ConstraintReconciliation {
5856
val retainedStandard =
5957
if (standard.isEmpty) None
6058
else if (standard.lengthCompare(1) == 0) standard.headOption
61-
else {
62-
val parsedConstraints = standard.map(VersionParse.versionConstraint)
63-
VersionConstraint.merge(parsedConstraints: _*)
64-
.flatMap(_.repr)
65-
}
59+
else
60+
VersionConstraint.merge(standard: _*)
61+
.map(_.uniquePreferred.removeUnusedPreferred)
6662
val retainedLatestOpt = retainLatestOpt(latests)
6763

68-
if (standard.isEmpty)
69-
retainedLatestOpt
70-
else if (latests.isEmpty)
71-
retainedStandard
64+
if (standard.isEmpty) retainedLatestOpt
65+
else if (latests.isEmpty) retainedStandard
7266
else {
73-
val parsedIntervals = standard.map(VersionParse.versionConstraint)
67+
val parsedIntervals = standard
7468
.filter(_.preferred.isEmpty) // only keep intervals
7569
.filter(_.interval != VersionInterval.zero) // not interval matching any version
7670

7771
if (parsedIntervals.isEmpty)
7872
retainedLatestOpt
7973
else
8074
VersionConstraint.merge(parsedIntervals: _*)
81-
.flatMap(_.repr)
82-
.map(itv => (itv +: retainedLatestOpt.toSeq).mkString("&"))
75+
.map(_.uniquePreferred.removeUnusedPreferred) // FIXME Add retainedLatestOpt too
8376
}
8477
}
8578
}
@@ -90,7 +83,7 @@ object ConstraintReconciliation {
9083
* When passed version intervals that don't overlap, the lowest intervals are discarded until the remaining intervals do overlap.
9184
*/
9285
case object Relaxed extends ConstraintReconciliation {
93-
def reconcile(versions: Seq[String]): Option[String] =
86+
def reconcile(versions: Seq[VersionConstraint]): Option[VersionConstraint] =
9487
if (versions.isEmpty)
9588
None
9689
else if (versions.lengthCompare(1) == 0)
@@ -101,16 +94,15 @@ object ConstraintReconciliation {
10194
if (standard.isEmpty) None
10295
else if (standard.lengthCompare(1) == 0) standard.headOption
10396
else {
104-
val parsedConstraints = standard.map(VersionParse.versionConstraint)
105-
VersionConstraint.merge(parsedConstraints: _*)
106-
.getOrElse(VersionConstraint.relaxedMerge(parsedConstraints: _*))
107-
.repr
97+
val repr = VersionConstraint.merge(standard: _*)
98+
.getOrElse(VersionConstraint.relaxedMerge(standard: _*))
99+
.uniquePreferred
100+
.removeUnusedPreferred
101+
Some(repr)
108102
}
109103
val retainedLatestOpt = retainLatestOpt(latests)
110-
if (latests.isEmpty)
111-
retainedStandard
112-
else
113-
retainedLatestOpt
104+
if (latests.isEmpty) retainedStandard
105+
else retainedLatestOpt
114106
}
115107
}
116108

@@ -129,4 +121,36 @@ object ConstraintReconciliation {
129121
case _ => Default
130122
}
131123

124+
/** Strict version reconciliation.
125+
*
126+
* This particular instance behaves the same as [[Default]] when used by
127+
* [[coursier.core.Resolution]]. Actual strict conflict manager is handled by
128+
* `coursier.params.rule.Strict`, which is set up by `coursier.Resolve` when a strict
129+
* reconciliation is added to it.
130+
*/
131+
case object Strict extends ConstraintReconciliation {
132+
def reconcile(versions: Seq[VersionConstraint]): Option[VersionConstraint] =
133+
Default.reconcile(versions)
134+
}
135+
136+
/** Semantic versioning version reconciliation.
137+
*
138+
* This particular instance behaves the same as [[Default]] when used by
139+
* [[coursier.core.Resolution]]. Actual semantic versioning checks are handled by
140+
* `coursier.params.rule.Strict` with field `semVer = true`, which is set up by
141+
* `coursier.Resolve` when a SemVer reconciliation is added to it.
142+
*/
143+
case object SemVer extends ConstraintReconciliation {
144+
def reconcile(versions: Seq[VersionConstraint]): Option[VersionConstraint] =
145+
Default.reconcile(versions)
146+
}
147+
148+
def apply(input: String): Option[ConstraintReconciliation] =
149+
input match {
150+
case "default" => Some(Default)
151+
case "relaxed" => Some(Relaxed)
152+
case "strict" => Some(Strict)
153+
case "semver" => Some(SemVer)
154+
case _ => None
155+
}
132156
}

versions/shared/src/coursier/version/ModuleMatcher.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ import scala.util.matching.Regex
2020
lazy val orgPattern = blobToPattern(organizationMatcher)
2121
lazy val namePattern = blobToPattern(nameMatcher)
2222
lazy val attributesPattern = attributeMatchers
23-
.mapValues(blobToPattern(_))
23+
.iterator
24+
.map {
25+
case (k, v) =>
26+
(k, blobToPattern(v))
27+
}
2428
.toMap
2529

2630
def matches(organization: String, name: String): Boolean =

versions/shared/src/coursier/version/Version.scala

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@ import scala.annotation.tailrec
1111
* Same kind of ordering as aether-util/src/main/java/org/eclipse/aether/util/version/GenericVersion.java
1212
*/
1313
@data class Version(repr: String) extends Ordered[Version] {
14-
lazy val items: Vector[Version.Item] = Version.items(repr)
14+
def asString: String = repr
15+
private var items0: Vector[Version.Item] = null
16+
def items: Vector[Version.Item] = {
17+
// no need to guard against concurrent computations, this is not too expensive to compute
18+
if (items0 == null)
19+
items0 = Version.items(repr)
20+
items0
21+
}
1522
def compare(other: Version) = Version.listCompare(items, other.items)
1623
def isEmpty = items.forall(_.isEmpty)
1724

@@ -21,11 +28,15 @@ import scala.annotation.tailrec
2128
repr
2229
.split(Array('.', '-'))
2330
.forall(_.lengthCompare(5) <= 0)
31+
32+
override lazy val hashCode = repr.hashCode()
2433
}
2534

2635
object Version {
2736

28-
private[version] val zero = Version("0")
37+
private val zero0 = Version("0")
38+
39+
def zero: Version = Version("0")
2940

3041
sealed abstract class Item extends Ordered[Item] {
3142
def compare(other: Item): Int =

0 commit comments

Comments
 (0)