From c0c88eda9f150c854c802aff3b35f3fd09379dab Mon Sep 17 00:00:00 2001 From: tanishiking24 Date: Sun, 6 May 2018 17:48:52 +0900 Subject: [PATCH 1/4] Add semantic version number selector API. This semantic version number selector API is based on - https://draftin.com/documents/375100?token=rR30GmJJzi4l3BRlD-cHs8lcAcdDAXH4oTzqOWeL0CT0BNv3PZEx0g8pBkI13sQgYXTBqShZ0Ucsqek3Fn3d-aU - https://docs.npmjs.com/misc/semver --- .../librarymanagement/SemanticSelector.scala | 259 ++++++++++++++ .../sbt/librarymanagement/VersionNumber.scala | 4 + .../SemanticSelectorSpec.scala | 315 ++++++++++++++++++ 3 files changed, 578 insertions(+) create mode 100644 core/src/main/scala/sbt/librarymanagement/SemanticSelector.scala create mode 100644 core/src/test/scala/sbt/librarymanagement/SemanticSelectorSpec.scala diff --git a/core/src/main/scala/sbt/librarymanagement/SemanticSelector.scala b/core/src/main/scala/sbt/librarymanagement/SemanticSelector.scala new file mode 100644 index 000000000..b728c939a --- /dev/null +++ b/core/src/main/scala/sbt/librarymanagement/SemanticSelector.scala @@ -0,0 +1,259 @@ +package sbt.librarymanagement + +/** + * Semantic version selector API to check if the VersionNumber satisfies + * conditions described by semantic version selector. + */ +sealed abstract case class SemanticSelector( + private val selectors: Seq[SemanticSelector.SemSelAndChunk]) { + + /** + * Check if the version number satisfies the conditions described by semantic version selector. + * + * The empty fields of the version number are assumed to be 0, for example, `1` is treated as `1.0.0`. + * + * @param versionNumber The Version Number to be checked if it satisfies the conditions. + * @return The result of checking the version number satisfies the conditions or not. + */ + def matches(versionNumber: VersionNumber): Boolean = { + selectors.exists(_.matches(versionNumber)) + } + override def toString: String = selectors.map(_.toString).mkString(" || ") +} +object SemanticSelector { + + /** + * Build a SemanticSelector that can match specific semantic versions. + * + * A `comparator` generally consist of an operator and version specifier. + * The set of operators is + * - `<`: Less than + * - `<=`: Less than or equal to + * - `>`: Greater than + * - `>=`: Greater than or equal to + * - `=`: Equal + * If no operator is specified, `=` is assumed. + * + * If minor or patch versions are not specified, some numbers are assumed. + * - `<=1.0` is equivalent to `<1.1.0`. + * - `<1.0` is equivalent to `<1.0.0`. + * - `>=1.0` is equivalent to `>=1.0.0`. + * - `>1.0` is equivalent to `>=1.1.0`. + * - `=1.0` is equivalent to `>=1.0 <=1.0` (so `>=1.0.0 <1.1.0`). + * + * Comparators can be combined by spaces to form the intersection set of the comparators. + * For example, `>1.2.3 <4.5.6` matches versions that are `greater than 1.2.3 AND less than 4.5.6`. + * + * The (intersection) set of comparators can combined by ` || ` (spaces are required) to form the + * union set of the intersection sets. So the semantic selector is in disjunctive normal form. + * + * Metadata and pre-release of VersionNumber are ignored. + * So `1.0.0` matches any versions that have `1.0.0` as normal version with any pre-release version + * or any metadata like `1.0.0-alpha`, `1.0.0+metadata`. + * + * Wildcard (`x`, `X`, `*`) can be used to match any number of minor or patch version. + * Actually, `1.0.x` is equivalent to `=1.0` (that is equivalent to `>=1.0.0 <1.1.0`) + * + * The hyphen range like `1.2.3 - 4.5.6` matches inclusive set of versions. + * So `1.2.3 - 4.5.6` is equivalent to `>=1.2.3 <=4.5.6`. + * Both sides of comparators around - are required and they can not have any operators. + * For example, `>=1.2.3 - 4.5.6` is invalid. + * + * @param selector A string that represents semantic version selector. + * @return A `SemanticSelector` that can match only onto specific semantic versions. + * @throws java.lang.IllegalAccessException when selector is in invalid format of semantic version selector. + */ + def apply(selector: String): SemanticSelector = { + val orChunkTokens = selector.split("\\s+\\|\\|\\s+").map(_.trim) + val orChunks = orChunkTokens.map { chunk => + SemSelAndChunk(chunk) + } + new SemanticSelector(orChunks) {} + } + + private[this] sealed trait SemSelOperator + private[this] case object Lte extends SemSelOperator { + override def toString: String = "<=" + } + private[this] case object Lt extends SemSelOperator { + override def toString: String = "<" + } + private[this] case object Gte extends SemSelOperator { + override def toString: String = ">=" + } + private[this] case object Gt extends SemSelOperator { + override def toString: String = ">" + } + private[this] case object Eq extends SemSelOperator { + override def toString: String = "=" + } + + private[SemanticSelector] final case class SemSelAndChunk(comparators: Seq[SemComparator]) { + def matches(version: VersionNumber): Boolean = { + comparators.forall(_.matches(version)) + } + override def toString: String = comparators.map(_.toString).mkString(" ") + } + private[SemanticSelector] object SemSelAndChunk { + def apply(andClauseToken: String): SemSelAndChunk = parse(andClauseToken) + private[this] def parse(andClauseToken: String): SemSelAndChunk = { + val comparatorTokens = andClauseToken.split("\\s+") + val hyphenIndex = comparatorTokens.indexWhere(_ == "-") + val comparators = if (hyphenIndex == -1) { + comparatorTokens.map(SemComparator.apply) + } else { + // interpret `A.B.C - D.E.F` to `>=A.B.C <=D.E.F` + val (before, after) = comparatorTokens.splitAt(hyphenIndex) + (before.lastOption, after.drop(1).headOption) match { + case (Some(fromStr), Some(toStr)) => + // from and to can not have an operator. + if (hasOperator(fromStr) || hasOperator(toStr)) { + throw new IllegalArgumentException( + s"Invalid ' - ' range, both side of comparators can not have an operator: $fromStr - $toStr") + } + val from = SemComparator(fromStr) + val to = SemComparator(toStr) + val comparatorsBefore = before.dropRight(1).map(SemComparator.apply) + val comparatorsAfter = after.drop(2) match { + case tokens if !tokens.isEmpty => + parse(tokens.mkString(" ")).comparators + case _ => Seq.empty + } + from.copy(op = Gte) +: + to.copy(op = Lte) +: + (comparatorsBefore ++ comparatorsAfter) + case _ => + throw new IllegalArgumentException( + s"Invalid ' - ' range position, both side of versions must be specified: $andClauseToken") + } + } + SemSelAndChunk(comparators.flatMap(_.expandWildcard)) + } + + private[this] def hasOperator(comparator: String): Boolean = { + comparator.startsWith("<") || + comparator.startsWith(">") || + comparator.startsWith("=") + } + } + + private[SemanticSelector] final case class SemComparator private ( + op: SemSelOperator, + major: Option[Long], + minor: Option[Long], + patch: Option[Long] + ) { + def matches(version: VersionNumber): Boolean = { + // Fill empty fields of version specifier with 0 or max value of Long. + // By filling them, SemComparator realize the properties below + // `<=1.0` is equivalent to `<1.1.0` (`<=1.0.${Long.MaxValue}`) + // `<1.0` is equivalent to `<1.0.0` + // `>=1.0` is equivalent to `>=1.0.0` + // `>1.0` is equivalent to `>=1.1.0` (`>1.0.${Long.MaxValue}`) + // + // However this fills 0 for a comparator that have `=` operator, + // a comparator that have empty part of version and `=` operator won't appear + // because of expanding it to and clause of comparators. + val assumed = op match { + case Lte => Long.MaxValue + case Lt => 0L + case Gte => 0L + case Gt => Long.MaxValue + case Eq => 0L + } + // empty fields of the version number are assumed to be 0. + val versionNumber = + (version._1.getOrElse(0L), version._2.getOrElse(0L), version._3.getOrElse(0L)) + val selector = (major.getOrElse(assumed), minor.getOrElse(assumed), patch.getOrElse(assumed)) + val cmp = implicitly[Ordering[(Long, Long, Long)]].compare(versionNumber, selector) + op match { + case Lte if cmp <= 0 => true + case Lt if cmp < 0 => true + case Gte if cmp >= 0 => true + case Gt if cmp > 0 => true + case Eq if cmp == 0 => true + case _ => false + } + } + + // Expand wildcard with `=` operator to and clause of comparators. + // `=1.0` is equivalent to `>=1.0 <=1.0` + def expandWildcard: Seq[SemComparator] = { + if (op == Eq && !allFieldsSpecified) { + Seq(this.copy(op = Gte), this.copy(op = Lte)) + } else { + Seq(this) + } + } + private[this] def allFieldsSpecified: Boolean = + major.isDefined && minor.isDefined && patch.isDefined + + override def toString: String = { + val versionStr = Seq(major, minor, patch) + .collect { + case Some(v) => v.toString + } + .mkString(".") + s"$op$versionStr" + } + } + private[SemanticSelector] object SemComparator { + def apply(comparator: String): SemComparator = parse(comparator) + private[this] val ComparatorRegex = """(?x)^ + ([<>]=?|=)? + (?:(\d+|[xX*]) + (?:\.(\d+|[xX*]) + (?:\.(\d+|[xX*]))? + )? + )$ + """.r + private[this] def parse(comparator: String): SemComparator = { + comparator match { + case ComparatorRegex(rawOp, rawMajor, rawMinor, rawPatch) => + val opStr = Option(rawOp) + val major = Option(rawMajor) + val minor = Option(rawMinor) + val patch = Option(rawPatch) + + // Trim wildcard(x, X, *) and re-parse it. + // By trimming it, comparator realize the property like + // `=1.2.x` is equivalent to `=1.2`. + val hasXrangeSelector = Seq(major, minor, patch).exists { + case Some(str) => str.matches("[xX*]") + case None => false + } + if (hasXrangeSelector) { + val numbers = Seq(major, minor, patch).takeWhile { + case Some(str) => str.matches("\\d+") + case None => false + } + parse( + numbers + .collect { + case Some(v) => v.toString + } + .mkString(".") + ) + } else { + val operator = opStr match { + case Some("<") => Lt + case Some("<=") => Lte + case Some(">") => Gt + case Some(">=") => Gte + case Some("=") => Eq + case None => Eq + case Some(_) => + throw new IllegalArgumentException(s"Invalid operator: $opStr") + } + SemComparator( + operator, + major.map(_.toLong), + minor.map(_.toLong), + patch.map(_.toLong) + ) + } + case _ => throw new IllegalArgumentException(s"Invalid comparator: $comparator") + } + } + } +} diff --git a/core/src/main/scala/sbt/librarymanagement/VersionNumber.scala b/core/src/main/scala/sbt/librarymanagement/VersionNumber.scala index bee13b8f9..102d468c3 100644 --- a/core/src/main/scala/sbt/librarymanagement/VersionNumber.scala +++ b/core/src/main/scala/sbt/librarymanagement/VersionNumber.scala @@ -27,6 +27,10 @@ final class VersionNumber private[sbt] ( case _ => false } + def satisfies(selector: String): Boolean = { + SemanticSelector(selector).matches(this) + } + /** A variant of mkString that returns the empty string if the sequence is empty. */ private[this] def mkString1[A](xs: Seq[A], start: String, sep: String, end: String): String = if (xs.isEmpty) "" else xs.mkString(start, sep, end) diff --git a/core/src/test/scala/sbt/librarymanagement/SemanticSelectorSpec.scala b/core/src/test/scala/sbt/librarymanagement/SemanticSelectorSpec.scala new file mode 100644 index 000000000..4a33d45c5 --- /dev/null +++ b/core/src/test/scala/sbt/librarymanagement/SemanticSelectorSpec.scala @@ -0,0 +1,315 @@ +package sbt.librarymanagement + +import org.scalatest.{ FreeSpec, Matchers } + +class SemanticSelectorSpec extends FreeSpec with Matchers { + semsel("<=1.2.3") { sel => + assertMatches(sel, "1.2.3") + assertMatches(sel, "1.2.3-beta") + assertMatches(sel, "1.2") + assertMatches(sel, "1") + assertNotMatches(sel, "1.2.4") + assertNotMatches(sel, "1.3") + assertNotMatches(sel, "1.3.0") + assertNotMatches(sel, "2") + } + + semsel("<=1.2") { sel => + assertMatches(sel, "1.2.3") + assertMatches(sel, "1.2.3-beta") + assertMatches(sel, "1.2") + assertMatches(sel, "1") + assertNotMatches(sel, "1.3.0") + } + + semsel("<=1") { sel => + assertMatches(sel, "1.12.12") + assertMatches(sel, "1.12.12-alpha") + assertMatches(sel, "1.2") + assertNotMatches(sel, "2.0.0") + assertNotMatches(sel, "2.0.0-alpha") + } + + semsel("<1.2.3") { sel => + assertMatches(sel, "1.2.2") + assertMatches(sel, "1.2") + assertNotMatches(sel, "1.2.3-alpha") + assertNotMatches(sel, "1.2.3") + } + + semsel("<1.2") { sel => + assertMatches(sel, "1.1.23") + assertMatches(sel, "1.1") + assertNotMatches(sel, "1.2") + assertNotMatches(sel, "1.2.0-alpha") + } + + semsel("<1") { sel => + assertMatches(sel, "0.9.12") + assertMatches(sel, "0.8") + assertNotMatches(sel, "1") + assertNotMatches(sel, "1.0") + assertNotMatches(sel, "1.0.0") + } + + semsel(">=1.2.3") { sel => + assertMatches(sel, "1.2.3") + assertMatches(sel, "1.2.3-beta") + assertMatches(sel, "1.3") + assertMatches(sel, "2") + assertNotMatches(sel, "1.2.2") + assertNotMatches(sel, "1.2") + assertNotMatches(sel, "1") + } + + semsel(">=1.2") { sel => + assertMatches(sel, "1.2.0") + assertMatches(sel, "1.2.0-beta") + assertMatches(sel, "1.2") + assertMatches(sel, "2") + assertNotMatches(sel, "1.1.23") + assertNotMatches(sel, "1.1") + assertNotMatches(sel, "1") + } + + semsel(">=1") { sel => + assertMatches(sel, "1.0.0") + assertMatches(sel, "1.0.0-beta") + assertMatches(sel, "1.0") + assertMatches(sel, "1") + assertNotMatches(sel, "0.9.9") + assertNotMatches(sel, "0.1") + assertNotMatches(sel, "0") + } + + semsel(">1.2.3") { sel => + assertMatches(sel, "1.2.4") + assertMatches(sel, "1.2.4-alpha") + assertMatches(sel, "1.3") + assertMatches(sel, "2") + assertNotMatches(sel, "1.2.3") + assertNotMatches(sel, "1.2") + assertNotMatches(sel, "1") + } + + semsel(">1.2") { sel => + assertMatches(sel, "1.3.0") + assertMatches(sel, "1.3.0-alpha") + assertMatches(sel, "1.3") + assertMatches(sel, "2") + assertNotMatches(sel, "1.2.9") + assertNotMatches(sel, "1.2") + assertNotMatches(sel, "1") + } + + semsel(">1") { sel => + assertMatches(sel, "2.0.0") + assertMatches(sel, "2.0") + assertMatches(sel, "2") + assertNotMatches(sel, "1.2.3") + assertNotMatches(sel, "1.2") + assertNotMatches(sel, "1") + } + + semsel("1.2.3") { sel => + assertMatches(sel, "1.2.3") + assertMatches(sel, "1.2.3-alpha") + assertNotMatches(sel, "1.2") + assertNotMatches(sel, "1.2.4") + } + + Seq(".x", ".X", ".*", ".x.x", "").foreach { xrange => + semsel(s"1$xrange") { sel => + assertMatches(sel, "1.0.0") + assertMatches(sel, "1.0.1") + assertMatches(sel, "1.1.1") + assertMatches(sel, "1.0.0-alpha") + assertNotMatches(sel, "2.0.0") + assertNotMatches(sel, "0.1.0") + } + } + + Seq(".x", ".X", ".*", "").foreach { xrange => + semsel(s"1.2$xrange") { sel => + assertMatches(sel, "1.2.0") + assertMatches(sel, "1.2.0-beta") + assertMatches(sel, "1.2.3") + assertNotMatches(sel, "1.3.0") + assertNotMatches(sel, "1.1.1") + } + } + + semsel("=1.2.3") { sel => + assertMatches(sel, "1.2.3") + assertMatches(sel, "1.2.3-alpha") + assertNotMatches(sel, "1.2") + assertNotMatches(sel, "1.2.4") + } + semsel("=1.2") { sel => + assertMatches(sel, "1.2.0") + assertMatches(sel, "1.2.0-alpha") + assertMatches(sel, "1.2") + assertMatches(sel, "1.2.1") + assertMatches(sel, "1.2.4") + } + semsel("=1") { sel => + assertMatches(sel, "1.0.0") + assertMatches(sel, "1.0.0-alpha") + assertMatches(sel, "1.0") + assertMatches(sel, "1.0.1") + assertMatches(sel, "1.2.3") + } + semsel("1.2.3 || 2.0.0") { sel => + assertMatches(sel, "1.2.3") + assertMatches(sel, "2.0.0") + assertNotMatches(sel, "1.2") + assertNotMatches(sel, "2.0.1") + } + semsel("<=1.2.3 || >=2.0.0 || 1.3.x") { sel => + assertMatches(sel, "1.0") + assertMatches(sel, "1.2.3") + assertMatches(sel, "2.0.0") + assertMatches(sel, "2.0") + assertMatches(sel, "1.3.0") + assertMatches(sel, "1.3.3") + assertNotMatches(sel, "1.2.4") + assertNotMatches(sel, "1.4.0") + } + + semsel(">=1.2.3 <2.0.0") { sel => + assertMatches(sel, "1.2.3") + assertMatches(sel, "1.9.9") + assertNotMatches(sel, "1.2") + assertNotMatches(sel, "2.0.0") + } + + semsel(">=1.2.3 <2.0.0 || >3.0.0 <=3.2.0") { sel => + assertMatches(sel, "1.2.3") + assertMatches(sel, "1.9.9") + assertMatches(sel, "3.0.1") + assertMatches(sel, "3.2.0") + assertNotMatches(sel, "1.2") + assertNotMatches(sel, "2.0.0") + assertNotMatches(sel, "3.0.0") + assertNotMatches(sel, "3.2.1") + } + + semsel("1.2.3 - 2.0.0") { sel => + assertMatches(sel, "1.2.3") + assertMatches(sel, "1.9.9") + assertMatches(sel, "2.0.0") + assertNotMatches(sel, "1.2") + assertNotMatches(sel, "2.0.1") + } + semsel("1.2 - 2") { sel => + assertMatches(sel, "1.2.0") + assertMatches(sel, "1.9.9") + assertMatches(sel, "2.0.0") + assertMatches(sel, "2.0.1") + assertNotMatches(sel, "1.1") + assertNotMatches(sel, "3.0.0") + } + semsel("1.2.3 - 2.0.0 1.5.0 - 2.4.0") { sel => + assertMatches(sel, "1.5.0") + assertMatches(sel, "1.9.9") + assertMatches(sel, "2.0.0") + assertNotMatches(sel, "1.2.3") + assertNotMatches(sel, "1.4") + assertNotMatches(sel, "2.0.1") + assertNotMatches(sel, "2.4.0") + } + semsel("1.2.3 - 2.0 || 2.4.0 - 3") { sel => + assertMatches(sel, "1.2.3") + assertMatches(sel, "1.5.0") + assertMatches(sel, "2.0.0") + assertMatches(sel, "2.4.0") + assertMatches(sel, "2.9") + assertMatches(sel, "3.0.0") + assertMatches(sel, "2.0.1") + assertMatches(sel, "3.0.1") + assertMatches(sel, "3.1.0") + assertNotMatches(sel, "2.1") + assertNotMatches(sel, "2.3.9") + assertNotMatches(sel, "4.0.0") + } + + semsel(">=1.x") { sel => + assertMatches(sel, "1.0.0") + assertMatches(sel, "1.0.0-beta") + assertMatches(sel, "1.0") + assertMatches(sel, "1") + assertNotMatches(sel, "0.9.9") + assertNotMatches(sel, "0.1") + assertNotMatches(sel, "0") + } + + Seq( + // invalid operator + "~1.2.3", + "<~1.2.3", + "+1.2.3", + "!1.0.0", + ">~1.2.3", + // too much version fields + "1.2.3.4", + "1.2.3.4.5", + "1.2.3.x", + // invalid version specifier + "string.!?", + "1.y", + "1.2x", + "1.1.c", + "-1", + "x", + "", + // || without spaces + "1.2.3|| 2.3.4", + "1.2.3 ||2.3.4", + "1.2.3||2.3.4", + // invalid - operator + "- 1.1.1", + "2.0.0 -", + "1.0.0 - 2.0.0 - 3.0.0", + ">=1.0.0 - 2.0.0", + "1.0.0 - =3.0.0", + "=1.0.0 - =3.0.0", + "1.0.0 - 2.0.0 || - 2.0.0", + "1.0.0- 2.0.0", + "1.0.0 -2.0.0", + "1.0.0-2.0.0", + "-", + // cannot specify pre-release or metadata + "1.2.3-alpha", + "1.2-alpha", + "1.2.3+meta" + ).foreach { selectorStr => + semsel(selectorStr) { sel => + assertParsesToError(sel) + } + } + + private[this] final class SemanticSelectorString(val value: String) + private[this] def semsel(s: String)(f: SemanticSelectorString => Unit): Unit = + s"""SemanticSelector "$s"""" - { + f(new SemanticSelectorString(s)) + } + + private[this] def assertMatches( + s: SemanticSelectorString, + v: String + ): Unit = s"""should match "$v"""" in { + SemanticSelector(s.value).matches(VersionNumber(v)) shouldBe true + } + + private[this] def assertNotMatches( + s: SemanticSelectorString, + v: String + ): Unit = s"""should not match "$v"""" in { + SemanticSelector(s.value).matches(VersionNumber(v)) shouldBe false + } + + private[this] def assertParsesToError(s: SemanticSelectorString): Unit = + s"""should parse as an error""" in { + an[IllegalArgumentException] should be thrownBy SemanticSelector(s.value) + } +} From f8efdb1ac2094bc1c8585009b02d5b45427cf880 Mon Sep 17 00:00:00 2001 From: tanishiking24 Date: Fri, 11 May 2018 02:15:43 +0900 Subject: [PATCH 2/4] Make semantic selector honor semver ordering around pre-release tags. - When major, minor, and patch are equal, a pre-release version has lower precedence than a normal version. Example: 1.0.0-alpha < 1.0.0. - Precedence for two pre-release versions with the same major, minor, and patch version MUST be determined by comparing each dot hyphen separated identifier from left to right until a difference is found as follows - identifiers consisting of only digits are compared numerically and identifiers with letters or hyphens are compared lexically in ASCII sort order. - Numeric identifiers always have lower precedence than non-numeric identifiers. - A larger set of pre-release fields has a higher precedence than a smaller set, if all of the preceding identifiers are equal. - Example: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0. https://semver.org/#spec-item-11 --- .../librarymanagement/SemanticSelector.scala | 70 ++++++++++-- .../SemanticSelectorSpec.scala | 106 ++++++++++++++---- 2 files changed, 148 insertions(+), 28 deletions(-) diff --git a/core/src/main/scala/sbt/librarymanagement/SemanticSelector.scala b/core/src/main/scala/sbt/librarymanagement/SemanticSelector.scala index b728c939a..279f4fdd0 100644 --- a/core/src/main/scala/sbt/librarymanagement/SemanticSelector.scala +++ b/core/src/main/scala/sbt/librarymanagement/SemanticSelector.scala @@ -1,5 +1,7 @@ package sbt.librarymanagement +import scala.annotation.tailrec + /** * Semantic version selector API to check if the VersionNumber satisfies * conditions described by semantic version selector. @@ -43,9 +45,9 @@ object SemanticSelector { * * Comparators can be combined by spaces to form the intersection set of the comparators. * For example, `>1.2.3 <4.5.6` matches versions that are `greater than 1.2.3 AND less than 4.5.6`. - * - * The (intersection) set of comparators can combined by ` || ` (spaces are required) to form the - * union set of the intersection sets. So the semantic selector is in disjunctive normal form. + * + * The (intersection) set of comparators can combined by ` || ` (spaces are required) to form the + * union set of the intersection sets. So the semantic selector is in disjunctive normal form. * * Metadata and pre-release of VersionNumber are ignored. * So `1.0.0` matches any versions that have `1.0.0` as normal version with any pre-release version @@ -141,7 +143,8 @@ object SemanticSelector { op: SemSelOperator, major: Option[Long], minor: Option[Long], - patch: Option[Long] + patch: Option[Long], + tags: Seq[String] ) { def matches(version: VersionNumber): Boolean = { // Fill empty fields of version specifier with 0 or max value of Long. @@ -165,7 +168,11 @@ object SemanticSelector { val versionNumber = (version._1.getOrElse(0L), version._2.getOrElse(0L), version._3.getOrElse(0L)) val selector = (major.getOrElse(assumed), minor.getOrElse(assumed), patch.getOrElse(assumed)) - val cmp = implicitly[Ordering[(Long, Long, Long)]].compare(versionNumber, selector) + val normalVersionCmp = + implicitly[Ordering[(Long, Long, Long)]].compare(versionNumber, selector) + val cmp = + if (normalVersionCmp == 0) SemComparator.comparePreReleaseTags(version.tags, tags) + else normalVersionCmp op match { case Lte if cmp <= 0 => true case Lt if cmp < 0 => true @@ -194,7 +201,8 @@ object SemanticSelector { case Some(v) => v.toString } .mkString(".") - s"$op$versionStr" + val tagsStr = if (tags.nonEmpty) s"-${tags.mkString("-")}" else "" + s"$op$versionStr$tagsStr" } } private[SemanticSelector] object SemComparator { @@ -205,15 +213,16 @@ object SemanticSelector { (?:\.(\d+|[xX*]) (?:\.(\d+|[xX*]))? )? - )$ + )((?:-\w+)*)$ """.r private[this] def parse(comparator: String): SemComparator = { comparator match { - case ComparatorRegex(rawOp, rawMajor, rawMinor, rawPatch) => + case ComparatorRegex(rawOp, rawMajor, rawMinor, rawPatch, ts) => val opStr = Option(rawOp) val major = Option(rawMajor) val minor = Option(rawMinor) val patch = Option(rawPatch) + val tags = splitDash(ts) // Trim wildcard(x, X, *) and re-parse it. // By trimming it, comparator realize the property like @@ -223,6 +232,9 @@ object SemanticSelector { case None => false } if (hasXrangeSelector) { + if (tags.nonEmpty) + throw new IllegalArgumentException( + s"Pre-release version requires major, minor, patch versions to be specified: $comparator") val numbers = Seq(major, minor, patch).takeWhile { case Some(str) => str.matches("\\d+") case None => false @@ -235,6 +247,9 @@ object SemanticSelector { .mkString(".") ) } else { + if (tags.nonEmpty && (major.isEmpty || minor.isEmpty || patch.isEmpty)) + throw new IllegalArgumentException( + s"Pre-release version requires major, minor, patch versions to be specified: $comparator") val operator = opStr match { case Some("<") => Lt case Some("<=") => Lte @@ -249,11 +264,48 @@ object SemanticSelector { operator, major.map(_.toLong), minor.map(_.toLong), - patch.map(_.toLong) + patch.map(_.toLong), + tags ) } case _ => throw new IllegalArgumentException(s"Invalid comparator: $comparator") } } + private[this] def splitOn[A](s: String, sep: Char): Vector[String] = + if (s eq null) Vector() + else s.split(sep).filterNot(_ == "").toVector + private[this] def splitDash(s: String) = splitOn(s, '-') + + private[SemComparator] def comparePreReleaseTags(ts1: Seq[String], ts2: Seq[String]): Int = { + // > When major, minor, and patch are equal, a pre-release version has lower precedence than a normal version. + if (ts1.isEmpty && ts2.isEmpty) 0 + else if (ts1.nonEmpty && ts2.isEmpty) -1 // ts1 is pre-release version + else if (ts1.isEmpty && ts2.nonEmpty) 1 // ts2 is pre-release version + else compareTags(ts1, ts2) + } + + @tailrec + private[this] def compareTags(ts1: Seq[String], ts2: Seq[String]): Int = { + // > A larger set of pre-release fields has a higher precedence than a smaller set, + // > if all of the preceding identifiers are equal. + if (ts1.isEmpty && ts2.isEmpty) 0 + else if (ts1.nonEmpty && ts2.isEmpty) 1 + else if (ts1.isEmpty && ts2.nonEmpty) -1 + else { + val ts1head = ts1.head + val ts2head = ts2.head + val cmp = (ts1head.matches("\\d+"), ts2head.matches("\\d+")) match { + // Identifiers consisting of only digits are compared numerically. + // Numeric identifiers always have lower precedence than non-numeric identifiers. + // Identifiers with letters are compared case insensitive lexical order. + case (true, true) => implicitly[Ordering[Long]].compare(ts1head.toLong, ts2head.toLong) + case (false, true) => 1 + case (true, false) => -1 + case (false, false) => ts1head.toLowerCase.compareTo(ts2head.toLowerCase) + } + if (cmp == 0) compareTags(ts1.tail, ts2.tail) + else cmp + } + } } } diff --git a/core/src/test/scala/sbt/librarymanagement/SemanticSelectorSpec.scala b/core/src/test/scala/sbt/librarymanagement/SemanticSelectorSpec.scala index 4a33d45c5..f7bf4f5bf 100644 --- a/core/src/test/scala/sbt/librarymanagement/SemanticSelectorSpec.scala +++ b/core/src/test/scala/sbt/librarymanagement/SemanticSelectorSpec.scala @@ -5,9 +5,11 @@ import org.scalatest.{ FreeSpec, Matchers } class SemanticSelectorSpec extends FreeSpec with Matchers { semsel("<=1.2.3") { sel => assertMatches(sel, "1.2.3") + assertMatches(sel, "1.2-beta") assertMatches(sel, "1.2.3-beta") assertMatches(sel, "1.2") assertMatches(sel, "1") + assertNotMatches(sel, "1.2.4-alpha") assertNotMatches(sel, "1.2.4") assertNotMatches(sel, "1.3") assertNotMatches(sel, "1.3.0") @@ -15,68 +17,89 @@ class SemanticSelectorSpec extends FreeSpec with Matchers { } semsel("<=1.2") { sel => - assertMatches(sel, "1.2.3") + assertMatches(sel, "1.2.345-beta") assertMatches(sel, "1.2.3-beta") + assertMatches(sel, "1.2.3") assertMatches(sel, "1.2") assertMatches(sel, "1") assertNotMatches(sel, "1.3.0") + assertNotMatches(sel, "1.3.0-alpha") } semsel("<=1") { sel => - assertMatches(sel, "1.12.12") - assertMatches(sel, "1.12.12-alpha") - assertMatches(sel, "1.2") + assertMatches(sel, "1.234.567-alpha") + assertMatches(sel, "1.234.567") + assertMatches(sel, "1.234") + assertMatches(sel, "1.0.0-alpha") + assertMatches(sel, "1.0.0") + assertMatches(sel, "1.0") + assertMatches(sel, "1") assertNotMatches(sel, "2.0.0") assertNotMatches(sel, "2.0.0-alpha") } semsel("<1.2.3") { sel => + assertMatches(sel, "1.2.3-alpha") assertMatches(sel, "1.2.2") assertMatches(sel, "1.2") - assertNotMatches(sel, "1.2.3-alpha") + assertMatches(sel, "1") + assertNotMatches(sel, "1.2.4-beta") assertNotMatches(sel, "1.2.3") + assertNotMatches(sel, "1.3") + assertNotMatches(sel, "2") } semsel("<1.2") { sel => + assertMatches(sel, "1.2.0-alpha") assertMatches(sel, "1.1.23") assertMatches(sel, "1.1") + assertMatches(sel, "1") + assertNotMatches(sel, "1.3-beta") + assertNotMatches(sel, "1.2.0") assertNotMatches(sel, "1.2") - assertNotMatches(sel, "1.2.0-alpha") + assertNotMatches(sel, "2") } semsel("<1") { sel => + assertMatches(sel, "1.0.0-beta") + assertMatches(sel, "0.9.9-beta") assertMatches(sel, "0.9.12") assertMatches(sel, "0.8") + assertMatches(sel, "0") + assertNotMatches(sel, "1.0.1-beta") assertNotMatches(sel, "1") assertNotMatches(sel, "1.0") assertNotMatches(sel, "1.0.0") } semsel(">=1.2.3") { sel => + assertMatches(sel, "1.2.4-beta") assertMatches(sel, "1.2.3") - assertMatches(sel, "1.2.3-beta") assertMatches(sel, "1.3") assertMatches(sel, "2") + assertNotMatches(sel, "1.2.3-beta") assertNotMatches(sel, "1.2.2") assertNotMatches(sel, "1.2") assertNotMatches(sel, "1") } semsel(">=1.2") { sel => + assertMatches(sel, "1.2.1-beta") assertMatches(sel, "1.2.0") - assertMatches(sel, "1.2.0-beta") assertMatches(sel, "1.2") assertMatches(sel, "2") + assertNotMatches(sel, "1.2.0-beta") assertNotMatches(sel, "1.1.23") assertNotMatches(sel, "1.1") assertNotMatches(sel, "1") } semsel(">=1") { sel => + assertMatches(sel, "1.0.1-beta") assertMatches(sel, "1.0.0") - assertMatches(sel, "1.0.0-beta") assertMatches(sel, "1.0") assertMatches(sel, "1") + assertNotMatches(sel, "1.0.0-beta") assertNotMatches(sel, "0.9.9") assertNotMatches(sel, "0.1") assertNotMatches(sel, "0") @@ -87,6 +110,7 @@ class SemanticSelectorSpec extends FreeSpec with Matchers { assertMatches(sel, "1.2.4-alpha") assertMatches(sel, "1.3") assertMatches(sel, "2") + assertNotMatches(sel, "1.2.3-alpha") assertNotMatches(sel, "1.2.3") assertNotMatches(sel, "1.2") assertNotMatches(sel, "1") @@ -97,15 +121,18 @@ class SemanticSelectorSpec extends FreeSpec with Matchers { assertMatches(sel, "1.3.0-alpha") assertMatches(sel, "1.3") assertMatches(sel, "2") + assertNotMatches(sel, "1.2.0-alpha") assertNotMatches(sel, "1.2.9") assertNotMatches(sel, "1.2") assertNotMatches(sel, "1") } semsel(">1") { sel => + assertMatches(sel, "2.0.0-alpha") assertMatches(sel, "2.0.0") assertMatches(sel, "2.0") assertMatches(sel, "2") + assertNotMatches(sel, "1.2.3-alpha") assertNotMatches(sel, "1.2.3") assertNotMatches(sel, "1.2") assertNotMatches(sel, "1") @@ -113,17 +140,19 @@ class SemanticSelectorSpec extends FreeSpec with Matchers { semsel("1.2.3") { sel => assertMatches(sel, "1.2.3") - assertMatches(sel, "1.2.3-alpha") + assertNotMatches(sel, "1.2.3-alpha") assertNotMatches(sel, "1.2") assertNotMatches(sel, "1.2.4") } Seq(".x", ".X", ".*", ".x.x", "").foreach { xrange => semsel(s"1$xrange") { sel => + assertMatches(sel, "1.2.3-alpha") assertMatches(sel, "1.0.0") assertMatches(sel, "1.0.1") assertMatches(sel, "1.1.1") - assertMatches(sel, "1.0.0-alpha") + assertNotMatches(sel, "1.0.0-alpha") + assertNotMatches(sel, "2.0.0-alpha") assertNotMatches(sel, "2.0.0") assertNotMatches(sel, "0.1.0") } @@ -132,8 +161,10 @@ class SemanticSelectorSpec extends FreeSpec with Matchers { Seq(".x", ".X", ".*", "").foreach { xrange => semsel(s"1.2$xrange") { sel => assertMatches(sel, "1.2.0") - assertMatches(sel, "1.2.0-beta") assertMatches(sel, "1.2.3") + assertNotMatches(sel, "1.2.0-alpha") + assertNotMatches(sel, "1.2.0-beta") + assertNotMatches(sel, "1.3.0-beta") assertNotMatches(sel, "1.3.0") assertNotMatches(sel, "1.1.1") } @@ -141,23 +172,27 @@ class SemanticSelectorSpec extends FreeSpec with Matchers { semsel("=1.2.3") { sel => assertMatches(sel, "1.2.3") - assertMatches(sel, "1.2.3-alpha") + assertNotMatches(sel, "1.2.3-alpha") assertNotMatches(sel, "1.2") assertNotMatches(sel, "1.2.4") } semsel("=1.2") { sel => assertMatches(sel, "1.2.0") - assertMatches(sel, "1.2.0-alpha") assertMatches(sel, "1.2") assertMatches(sel, "1.2.1") assertMatches(sel, "1.2.4") + assertNotMatches(sel, "1.1.0") + assertNotMatches(sel, "1.3.0") + assertNotMatches(sel, "1.2.0-alpha") + assertNotMatches(sel, "1.3.0-alpha") } semsel("=1") { sel => assertMatches(sel, "1.0.0") - assertMatches(sel, "1.0.0-alpha") assertMatches(sel, "1.0") assertMatches(sel, "1.0.1") assertMatches(sel, "1.2.3") + assertNotMatches(sel, "1.0.0-alpha") + assertNotMatches(sel, "2.0.0") } semsel("1.2.3 || 2.0.0") { sel => assertMatches(sel, "1.2.3") @@ -235,14 +270,41 @@ class SemanticSelectorSpec extends FreeSpec with Matchers { semsel(">=1.x") { sel => assertMatches(sel, "1.0.0") - assertMatches(sel, "1.0.0-beta") assertMatches(sel, "1.0") assertMatches(sel, "1") + assertNotMatches(sel, "1.0.0-beta") assertNotMatches(sel, "0.9.9") assertNotMatches(sel, "0.1") assertNotMatches(sel, "0") } + semsel(">=1.2.3-beta") { sel => + assertMatches(sel, "1.3-alpha") + assertMatches(sel, "1.2.3") + assertMatches(sel, "1.2.3-beta") + assertMatches(sel, "1.2.3-beta-2") + assertMatches(sel, "1.2.3-beta-gamma") + assertMatches(sel, "1.2.4") + assertMatches(sel, "1.3") + assertNotMatches(sel, "1.2.3-alpha") + assertNotMatches(sel, "1.2.2") + } + + semsel(">=1.2.3-beta-2") { sel => + assertMatches(sel, "1.3-alpha") + assertMatches(sel, "1.2.3") + assertMatches(sel, "1.2.3-beta-2") + assertMatches(sel, "1.2.3-beta-2-3") + assertMatches(sel, "1.2.3-beta-3") + assertMatches(sel, "1.2.3-beta-gamma") + assertMatches(sel, "1.2.4") + assertMatches(sel, "1.3") + assertNotMatches(sel, "1.2.3-alpha-3") + assertNotMatches(sel, "1.2.3-beta-1") + assertNotMatches(sel, "1.2.3-beta") + assertNotMatches(sel, "1.2.2") + } + Seq( // invalid operator "~1.2.3", @@ -278,9 +340,15 @@ class SemanticSelectorSpec extends FreeSpec with Matchers { "1.0.0 -2.0.0", "1.0.0-2.0.0", "-", - // cannot specify pre-release or metadata - "1.2.3-alpha", - "1.2-alpha", + // minor and patch versions are required for pre-release version + "1.2-alpha-beta", + "1-beta", + "<=1.2-beta", + "<=1-beta", + "1.2-beta - 1.3-alpha", + "1.2.x-beta", + "1.x.*-beta", + // cannot specify metadata "1.2.3+meta" ).foreach { selectorStr => semsel(selectorStr) { sel => From 4e8b6dc7aa42f022edea9ae798de1d7aac86404d Mon Sep 17 00:00:00 2001 From: tanishiking24 Date: Sat, 12 May 2018 11:05:22 +0900 Subject: [PATCH 3/4] Use contraband for generating SemanticSelector instead of using case class. --- .../librarymanagement/SemComparator.scala | 56 ++++ .../librarymanagement/SemSelAndChunk.scala | 32 ++ .../librarymanagement/SemanticSelector.scala | 74 +++++ .../main/contraband/librarymanagement2.json | 104 ++++++ .../librarymanagement/SemSelOperator.scala | 17 + .../SemanticSelectorExtra.scala | 209 ++++++++++++ .../librarymanagement/SemanticSelector.scala | 311 ------------------ 7 files changed, 492 insertions(+), 311 deletions(-) create mode 100644 core/src/main/contraband-scala/sbt/internal/librarymanagement/SemComparator.scala create mode 100644 core/src/main/contraband-scala/sbt/internal/librarymanagement/SemSelAndChunk.scala create mode 100644 core/src/main/contraband-scala/sbt/librarymanagement/SemanticSelector.scala create mode 100644 core/src/main/scala/sbt/internal/librarymanagement/SemSelOperator.scala create mode 100644 core/src/main/scala/sbt/internal/librarymanagement/SemanticSelectorExtra.scala delete mode 100644 core/src/main/scala/sbt/librarymanagement/SemanticSelector.scala diff --git a/core/src/main/contraband-scala/sbt/internal/librarymanagement/SemComparator.scala b/core/src/main/contraband-scala/sbt/internal/librarymanagement/SemComparator.scala new file mode 100644 index 000000000..fd3ff81db --- /dev/null +++ b/core/src/main/contraband-scala/sbt/internal/librarymanagement/SemComparator.scala @@ -0,0 +1,56 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.librarymanagement +final class SemComparator private ( + val op: sbt.internal.librarymanagement.SemSelOperator, + val major: Option[Long], + val minor: Option[Long], + val patch: Option[Long], + val tags: Seq[String]) extends sbt.internal.librarymanagement.SemComparatorExtra with Serializable { + def matches(version: sbt.librarymanagement.VersionNumber): Boolean = this.matchesImpl(version) + def expandWildcard: Seq[SemComparator] = { + if (op == sbt.internal.librarymanagement.SemSelOperator.Eq && !allFieldsSpecified) { + Seq( + this.withOp(sbt.internal.librarymanagement.SemSelOperator.Gte), + this.withOp(sbt.internal.librarymanagement.SemSelOperator.Lte) + ) + } else { Seq(this) } +} + + +override def equals(o: Any): Boolean = o match { + case x: SemComparator => (this.op == x.op) && (this.major == x.major) && (this.minor == x.minor) && (this.patch == x.patch) && (this.tags == x.tags) + case _ => false +} +override def hashCode: Int = { + 37 * (37 * (37 * (37 * (37 * (37 * (17 + "sbt.internal.librarymanagement.SemComparator".##) + op.##) + major.##) + minor.##) + patch.##) + tags.##) +} +override def toString: String = { + this.toStringImpl +} +private[this] def copy(op: sbt.internal.librarymanagement.SemSelOperator = op, major: Option[Long] = major, minor: Option[Long] = minor, patch: Option[Long] = patch, tags: Seq[String] = tags): SemComparator = { + new SemComparator(op, major, minor, patch, tags) +} +def withOp(op: sbt.internal.librarymanagement.SemSelOperator): SemComparator = { + copy(op = op) +} +def withMajor(major: Option[Long]): SemComparator = { + copy(major = major) +} +def withMinor(minor: Option[Long]): SemComparator = { + copy(minor = minor) +} +def withPatch(patch: Option[Long]): SemComparator = { + copy(patch = patch) +} +def withTags(tags: Seq[String]): SemComparator = { + copy(tags = tags) +} +} +object SemComparator extends sbt.internal.librarymanagement.SemComparatorFunctions { + def apply(comparator: String): SemComparator = parse(comparator) + def apply(op: sbt.internal.librarymanagement.SemSelOperator, major: Option[Long], minor: Option[Long], patch: Option[Long], tags: Seq[String]): SemComparator = new SemComparator(op, major, minor, patch, tags) +} diff --git a/core/src/main/contraband-scala/sbt/internal/librarymanagement/SemSelAndChunk.scala b/core/src/main/contraband-scala/sbt/internal/librarymanagement/SemSelAndChunk.scala new file mode 100644 index 000000000..9e25e9789 --- /dev/null +++ b/core/src/main/contraband-scala/sbt/internal/librarymanagement/SemSelAndChunk.scala @@ -0,0 +1,32 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.librarymanagement +final class SemSelAndChunk private ( + val comparators: Seq[sbt.internal.librarymanagement.SemComparator]) extends Serializable { + def matches(version: sbt.librarymanagement.VersionNumber): Boolean = comparators.forall(_.matches(version)) + + + override def equals(o: Any): Boolean = o match { + case x: SemSelAndChunk => (this.comparators == x.comparators) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (17 + "sbt.internal.librarymanagement.SemSelAndChunk".##) + comparators.##) + } + override def toString: String = { + comparators.map(_.toString).mkString(" ") + } + private[this] def copy(comparators: Seq[sbt.internal.librarymanagement.SemComparator] = comparators): SemSelAndChunk = { + new SemSelAndChunk(comparators) + } + def withComparators(comparators: Seq[sbt.internal.librarymanagement.SemComparator]): SemSelAndChunk = { + copy(comparators = comparators) + } +} +object SemSelAndChunk extends sbt.internal.librarymanagement.SemSelAndChunkFunctions { + def apply(andClauseToken: String): SemSelAndChunk = parse(andClauseToken) + def apply(comparators: Seq[sbt.internal.librarymanagement.SemComparator]): SemSelAndChunk = new SemSelAndChunk(comparators) +} diff --git a/core/src/main/contraband-scala/sbt/librarymanagement/SemanticSelector.scala b/core/src/main/contraband-scala/sbt/librarymanagement/SemanticSelector.scala new file mode 100644 index 000000000..65f6e5b19 --- /dev/null +++ b/core/src/main/contraband-scala/sbt/librarymanagement/SemanticSelector.scala @@ -0,0 +1,74 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.librarymanagement +/** + * Semantic version selector API to check if the VersionNumber satisfies + * conditions described by semantic version selector. + * + * A `comparator` generally consist of an operator and version specifier. + * The set of operators is + * - `<`: Less than + * - `<=`: Less than or equal to + * - `>`: Greater than + * - `>=`: Greater than or equal to + * - `=`: Equal + * If no operator is specified, `=` is assumed. + * + * If minor or patch versions are not specified, some numbers are assumed. + * - `<=1.0` is equivalent to `<1.1.0`. + * - `<1.0` is equivalent to `<1.0.0`. + * - `>=1.0` is equivalent to `>=1.0.0`. + * - `>1.0` is equivalent to `>=1.1.0`. + * - `=1.0` is equivalent to `>=1.0 <=1.0` (so `>=1.0.0 <1.1.0`). + * + * Comparators can be combined by spaces to form the intersection set of the comparators. + * For example, `>1.2.3 <4.5.6` matches versions that are `greater than 1.2.3 AND less than 4.5.6`. + * + * The (intersection) set of comparators can combined by ` || ` (spaces are required) to form the + * union set of the intersection sets. So the semantic selector is in disjunctive normal form. + * + * Metadata and pre-release of VersionNumber are ignored. + * So `1.0.0` matches any versions that have `1.0.0` as normal version with any pre-release version + * or any metadata like `1.0.0-alpha`, `1.0.0+metadata`. + * + * Wildcard (`x`, `X`, `*`) can be used to match any number of minor or patch version. + * Actually, `1.0.x` is equivalent to `=1.0` (that is equivalent to `>=1.0.0 <1.1.0`) + * + * The hyphen range like `1.2.3 - 4.5.6` matches inclusive set of versions. + * So `1.2.3 - 4.5.6` is equivalent to `>=1.2.3 <=4.5.6`. + * Both sides of comparators around - are required and they can not have any operators. + * For example, `>=1.2.3 - 4.5.6` is invalid. + */ +final class SemanticSelector private ( + val selectors: Seq[sbt.internal.librarymanagement.SemSelAndChunk]) extends Serializable { + def matches(versionNumber: VersionNumber): Boolean = selectors.exists(_.matches(versionNumber)) + + + override def equals(o: Any): Boolean = o match { + case x: SemanticSelector => (this.selectors == x.selectors) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (17 + "sbt.librarymanagement.SemanticSelector".##) + selectors.##) + } + override def toString: String = { + selectors.map(_.toString).mkString(" || ") + } + private[this] def copy(selectors: Seq[sbt.internal.librarymanagement.SemSelAndChunk] = selectors): SemanticSelector = { + new SemanticSelector(selectors) + } + def withSelectors(selectors: Seq[sbt.internal.librarymanagement.SemSelAndChunk]): SemanticSelector = { + copy(selectors = selectors) + } +} +object SemanticSelector { + def apply(selector: String): SemanticSelector = { + val orChunkTokens = selector.split("\\s+\\|\\|\\s+").map(_.trim) + val orChunks = orChunkTokens.map { chunk => sbt.internal.librarymanagement.SemSelAndChunk(chunk) } + SemanticSelector(orChunks) + } + def apply(selectors: Seq[sbt.internal.librarymanagement.SemSelAndChunk]): SemanticSelector = new SemanticSelector(selectors) +} diff --git a/core/src/main/contraband/librarymanagement2.json b/core/src/main/contraband/librarymanagement2.json index 2c9d3d6cf..f94cbef02 100644 --- a/core/src/main/contraband/librarymanagement2.json +++ b/core/src/main/contraband/librarymanagement2.json @@ -17,6 +17,110 @@ { "name": "includeTypes", "type": "Set[String]" } ], "parentsCompanion": "sbt.librarymanagement.MakePomConfigurationFunctions" + }, + { + "name": "SemanticSelector", + "namespace": "sbt.librarymanagement", + "target": "Scala", + "type": "record", + "doc": [ + "Semantic version selector API to check if the VersionNumber satisfies", + "conditions described by semantic version selector.", + "", + "A `comparator` generally consist of an operator and version specifier.", + "The set of operators is", + "- `<`: Less than", + "- `<=`: Less than or equal to", + "- `>`: Greater than", + "- `>=`: Greater than or equal to", + "- `=`: Equal", + "If no operator is specified, `=` is assumed.", + "", + "If minor or patch versions are not specified, some numbers are assumed.", + "- `<=1.0` is equivalent to `<1.1.0`.", + "- `<1.0` is equivalent to `<1.0.0`.", + "- `>=1.0` is equivalent to `>=1.0.0`.", + "- `>1.0` is equivalent to `>=1.1.0`.", + "- `=1.0` is equivalent to `>=1.0 <=1.0` (so `>=1.0.0 <1.1.0`).", + "", + "Comparators can be combined by spaces to form the intersection set of the comparators.", + "For example, `>1.2.3 <4.5.6` matches versions that are `greater than 1.2.3 AND less than 4.5.6`.", + "", + "The (intersection) set of comparators can combined by ` || ` (spaces are required) to form the", + "union set of the intersection sets. So the semantic selector is in disjunctive normal form.", + "", + "Metadata and pre-release of VersionNumber are ignored.", + "So `1.0.0` matches any versions that have `1.0.0` as normal version with any pre-release version", + "or any metadata like `1.0.0-alpha`, `1.0.0+metadata`.", + "", + "Wildcard (`x`, `X`, `*`) can be used to match any number of minor or patch version.", + "Actually, `1.0.x` is equivalent to `=1.0` (that is equivalent to `>=1.0.0 <1.1.0`)", + "", + "The hyphen range like `1.2.3 - 4.5.6` matches inclusive set of versions.", + "So `1.2.3 - 4.5.6` is equivalent to `>=1.2.3 <=4.5.6`.", + "Both sides of comparators around - are required and they can not have any operators.", + "For example, `>=1.2.3 - 4.5.6` is invalid." + ], + "generateCodec": false, + "fields": [ + { "name": "selectors", "type": "Seq[sbt.internal.librarymanagement.SemSelAndChunk]" } + ], + "toString": "selectors.map(_.toString).mkString(\" || \")", + "extra": "def matches(versionNumber: VersionNumber): Boolean = selectors.exists(_.matches(versionNumber))", + "extraCompanion": [ + "def apply(selector: String): SemanticSelector = {", + " val orChunkTokens = selector.split(\"\\\\s+\\\\|\\\\|\\\\s+\").map(_.trim)", + " val orChunks = orChunkTokens.map { chunk => sbt.internal.librarymanagement.SemSelAndChunk(chunk) }", + " SemanticSelector(orChunks)", + "}" + ] + }, + { + "name": "SemSelAndChunk", + "namespace": "sbt.internal.librarymanagement", + "target": "Scala", + "type": "record", + "generateCodec": false, + "parentsCompanion": "sbt.internal.librarymanagement.SemSelAndChunkFunctions", + "fields": [ + { "name": "comparators", "type": "Seq[sbt.internal.librarymanagement.SemComparator]" } + ], + "toString": "comparators.map(_.toString).mkString(\" \")", + "extra": "def matches(version: sbt.librarymanagement.VersionNumber): Boolean = comparators.forall(_.matches(version))", + "extraCompanion": [ + "def apply(andClauseToken: String): SemSelAndChunk = parse(andClauseToken)" + ] + }, + { + "name": "SemComparator", + "namespace": "sbt.internal.librarymanagement", + "target": "Scala", + "type": "record", + "generateCodec": false, + "toString": "this.toStringImpl", + "parents": "sbt.internal.librarymanagement.SemComparatorExtra", + "parentsCompanion": "sbt.internal.librarymanagement.SemComparatorFunctions", + "fields": [ + { "name": "op", "type": "sbt.internal.librarymanagement.SemSelOperator" }, + { "name": "major", "type": "Option[Long]" }, + { "name": "minor", "type": "Option[Long]" }, + { "name": "patch", "type": "Option[Long]" }, + { "name": "tags", "type": "Seq[String]" } + ], + "extra": [ + "def matches(version: sbt.librarymanagement.VersionNumber): Boolean = this.matchesImpl(version)", + "def expandWildcard: Seq[SemComparator] = {", + " if (op == sbt.internal.librarymanagement.SemSelOperator.Eq && !allFieldsSpecified) {", + " Seq(", + " this.withOp(sbt.internal.librarymanagement.SemSelOperator.Gte),", + " this.withOp(sbt.internal.librarymanagement.SemSelOperator.Lte)", + " )", + " } else { Seq(this) }", + "}" + ], + "extraCompanion": [ + "def apply(comparator: String): SemComparator = parse(comparator)" + ] } ] } diff --git a/core/src/main/scala/sbt/internal/librarymanagement/SemSelOperator.scala b/core/src/main/scala/sbt/internal/librarymanagement/SemSelOperator.scala new file mode 100644 index 000000000..d881fe9e2 --- /dev/null +++ b/core/src/main/scala/sbt/internal/librarymanagement/SemSelOperator.scala @@ -0,0 +1,17 @@ +package sbt.internal.librarymanagement +sealed abstract class SemSelOperator { + override def toString: String = this match { + case SemSelOperator.Lte => "<=" + case SemSelOperator.Lt => "<" + case SemSelOperator.Gte => ">=" + case SemSelOperator.Gt => ">" + case SemSelOperator.Eq => "=" + } +} +object SemSelOperator { + case object Lte extends SemSelOperator + case object Lt extends SemSelOperator + case object Gte extends SemSelOperator + case object Gt extends SemSelOperator + case object Eq extends SemSelOperator +} diff --git a/core/src/main/scala/sbt/internal/librarymanagement/SemanticSelectorExtra.scala b/core/src/main/scala/sbt/internal/librarymanagement/SemanticSelectorExtra.scala new file mode 100644 index 000000000..49990d1df --- /dev/null +++ b/core/src/main/scala/sbt/internal/librarymanagement/SemanticSelectorExtra.scala @@ -0,0 +1,209 @@ +package sbt.internal.librarymanagement + +import sbt.librarymanagement.VersionNumber +import sbt.internal.librarymanagement.SemSelOperator.{ Lt, Lte, Gt, Gte, Eq } + +import scala.annotation.tailrec + +private[librarymanagement] abstract class SemSelAndChunkFunctions { + protected def parse(andClauseToken: String): SemSelAndChunk = { + val comparatorTokens = andClauseToken.split("\\s+") + val hyphenIndex = comparatorTokens.indexWhere(_ == "-") + val comparators = if (hyphenIndex == -1) { + comparatorTokens.map(SemComparator.apply) + } else { + // interpret `A.B.C - D.E.F` to `>=A.B.C <=D.E.F` + val (before, after) = comparatorTokens.splitAt(hyphenIndex) + (before.lastOption, after.drop(1).headOption) match { + case (Some(fromStr), Some(toStr)) => + // from and to can not have an operator. + if (hasOperator(fromStr) || hasOperator(toStr)) { + throw new IllegalArgumentException( + s"Invalid ' - ' range, both side of comparators can not have an operator: $fromStr - $toStr") + } + val from = SemComparator(fromStr) + val to = SemComparator(toStr) + val comparatorsBefore = before.dropRight(1).map(SemComparator.apply) + val comparatorsAfter = after.drop(2) match { + case tokens if !tokens.isEmpty => + parse(tokens.mkString(" ")).comparators + case _ => Seq.empty + } + from.withOp(Gte) +: to.withOp(Lte) +: + (comparatorsBefore ++ comparatorsAfter) + case _ => + throw new IllegalArgumentException( + s"Invalid ' - ' range position, both side of versions must be specified: $andClauseToken") + } + } + SemSelAndChunk(comparators.flatMap(_.expandWildcard)) + } + + private[this] def hasOperator(comparator: String): Boolean = { + comparator.startsWith("<") || + comparator.startsWith(">") || + comparator.startsWith("=") + } +} + +private[librarymanagement] abstract class SemComparatorExtra { + val op: SemSelOperator + val major: Option[Long] + val minor: Option[Long] + val patch: Option[Long] + val tags: Seq[String] + + protected def toStringImpl: String = { + val versionStr = Seq(major, minor, patch) + .collect { + case Some(v) => v.toString + } + .mkString(".") + val tagsStr = if (tags.nonEmpty) s"-${tags.mkString("-")}" else "" + s"$op$versionStr$tagsStr" + } + + protected def matchesImpl(version: VersionNumber): Boolean = { + // Fill empty fields of version specifier with 0 or max value of Long. + // By filling them, SemComparator realize the properties below + // `<=1.0` is equivalent to `<1.1.0` (`<=1.0.${Long.MaxValue}`) + // `<1.0` is equivalent to `<1.0.0` + // `>=1.0` is equivalent to `>=1.0.0` + // `>1.0` is equivalent to `>=1.1.0` (`>1.0.${Long.MaxValue}`) + // + // However this fills 0 for a comparator that have `=` operator, + // a comparator that have empty part of version and `=` operator won't appear + // because of expanding it to and clause of comparators. + val assumed = op match { + case Lte => Long.MaxValue + case Lt => 0L + case Gte => 0L + case Gt => Long.MaxValue + case Eq => 0L + } + // empty fields of the version number are assumed to be 0. + val versionNumber = + (version._1.getOrElse(0L), version._2.getOrElse(0L), version._3.getOrElse(0L)) + val selector = (major.getOrElse(assumed), minor.getOrElse(assumed), patch.getOrElse(assumed)) + val normalVersionCmp = + implicitly[Ordering[(Long, Long, Long)]].compare(versionNumber, selector) + val cmp = + if (normalVersionCmp == 0) comparePreReleaseTags(version.tags, tags) + else normalVersionCmp + op match { + case Lte if cmp <= 0 => true + case Lt if cmp < 0 => true + case Gte if cmp >= 0 => true + case Gt if cmp > 0 => true + case Eq if cmp == 0 => true + case _ => false + } + } + private[this] def comparePreReleaseTags(ts1: Seq[String], ts2: Seq[String]): Int = { + // > When major, minor, and patch are equal, a pre-release version has lower precedence than a normal version. + if (ts1.isEmpty && ts2.isEmpty) 0 + else if (ts1.nonEmpty && ts2.isEmpty) -1 // ts1 is pre-release version + else if (ts1.isEmpty && ts2.nonEmpty) 1 // ts2 is pre-release version + else compareTags(ts1, ts2) + } + + @tailrec + private[this] def compareTags(ts1: Seq[String], ts2: Seq[String]): Int = { + // > A larger set of pre-release fields has a higher precedence than a smaller set, + // > if all of the preceding identifiers are equal. + if (ts1.isEmpty && ts2.isEmpty) 0 + else if (ts1.nonEmpty && ts2.isEmpty) 1 + else if (ts1.isEmpty && ts2.nonEmpty) -1 + else { + val ts1head = ts1.head + val ts2head = ts2.head + val cmp = (ts1head.matches("\\d+"), ts2head.matches("\\d+")) match { + // Identifiers consisting of only digits are compared numerically. + // Numeric identifiers always have lower precedence than non-numeric identifiers. + // Identifiers with letters are compared case insensitive lexical order. + case (true, true) => implicitly[Ordering[Long]].compare(ts1head.toLong, ts2head.toLong) + case (false, true) => 1 + case (true, false) => -1 + case (false, false) => ts1head.toLowerCase.compareTo(ts2head.toLowerCase) + } + if (cmp == 0) compareTags(ts1.tail, ts2.tail) + else cmp + } + } + + // Expand wildcard with `=` operator to and clause of comparators. + // `=1.0` is equivalent to `>=1.0 <=1.0` + protected def allFieldsSpecified: Boolean = + major.isDefined && minor.isDefined && patch.isDefined +} + +private[librarymanagement] abstract class SemComparatorFunctions { + private[this] val ComparatorRegex = """(?x)^ + ([<>]=?|=)? + (?:(\d+|[xX*]) + (?:\.(\d+|[xX*]) + (?:\.(\d+|[xX*]))? + )? + )((?:-\w+)*)$ + """.r + protected def parse(comparator: String): SemComparator = { + comparator match { + case ComparatorRegex(rawOp, rawMajor, rawMinor, rawPatch, ts) => + val opStr = Option(rawOp) + val major = Option(rawMajor) + val minor = Option(rawMinor) + val patch = Option(rawPatch) + val tags = splitDash(ts) + + // Trim wildcard(x, X, *) and re-parse it. + // By trimming it, comparator realize the property like + // `=1.2.x` is equivalent to `=1.2`. + val hasXrangeSelector = Seq(major, minor, patch).exists { + case Some(str) => str.matches("[xX*]") + case None => false + } + if (hasXrangeSelector) { + if (tags.nonEmpty) + throw new IllegalArgumentException( + s"Pre-release version requires major, minor, patch versions to be specified: $comparator") + val numbers = Seq(major, minor, patch).takeWhile { + case Some(str) => str.matches("\\d+") + case None => false + } + parse( + numbers + .collect { + case Some(v) => v.toString + } + .mkString(".") + ) + } else { + if (tags.nonEmpty && (major.isEmpty || minor.isEmpty || patch.isEmpty)) + throw new IllegalArgumentException( + s"Pre-release version requires major, minor, patch versions to be specified: $comparator") + val operator = opStr match { + case Some("<") => Lt + case Some("<=") => Lte + case Some(">") => Gt + case Some(">=") => Gte + case Some("=") => Eq + case None => Eq + case Some(_) => + throw new IllegalArgumentException(s"Invalid operator: $opStr") + } + SemComparator( + operator, + major.map(_.toLong), + minor.map(_.toLong), + patch.map(_.toLong), + tags + ) + } + case _ => throw new IllegalArgumentException(s"Invalid comparator: $comparator") + } + } + private[this] def splitOn[A](s: String, sep: Char): Vector[String] = + if (s eq null) Vector() + else s.split(sep).filterNot(_ == "").toVector + private[this] def splitDash(s: String) = splitOn(s, '-') +} diff --git a/core/src/main/scala/sbt/librarymanagement/SemanticSelector.scala b/core/src/main/scala/sbt/librarymanagement/SemanticSelector.scala deleted file mode 100644 index 279f4fdd0..000000000 --- a/core/src/main/scala/sbt/librarymanagement/SemanticSelector.scala +++ /dev/null @@ -1,311 +0,0 @@ -package sbt.librarymanagement - -import scala.annotation.tailrec - -/** - * Semantic version selector API to check if the VersionNumber satisfies - * conditions described by semantic version selector. - */ -sealed abstract case class SemanticSelector( - private val selectors: Seq[SemanticSelector.SemSelAndChunk]) { - - /** - * Check if the version number satisfies the conditions described by semantic version selector. - * - * The empty fields of the version number are assumed to be 0, for example, `1` is treated as `1.0.0`. - * - * @param versionNumber The Version Number to be checked if it satisfies the conditions. - * @return The result of checking the version number satisfies the conditions or not. - */ - def matches(versionNumber: VersionNumber): Boolean = { - selectors.exists(_.matches(versionNumber)) - } - override def toString: String = selectors.map(_.toString).mkString(" || ") -} -object SemanticSelector { - - /** - * Build a SemanticSelector that can match specific semantic versions. - * - * A `comparator` generally consist of an operator and version specifier. - * The set of operators is - * - `<`: Less than - * - `<=`: Less than or equal to - * - `>`: Greater than - * - `>=`: Greater than or equal to - * - `=`: Equal - * If no operator is specified, `=` is assumed. - * - * If minor or patch versions are not specified, some numbers are assumed. - * - `<=1.0` is equivalent to `<1.1.0`. - * - `<1.0` is equivalent to `<1.0.0`. - * - `>=1.0` is equivalent to `>=1.0.0`. - * - `>1.0` is equivalent to `>=1.1.0`. - * - `=1.0` is equivalent to `>=1.0 <=1.0` (so `>=1.0.0 <1.1.0`). - * - * Comparators can be combined by spaces to form the intersection set of the comparators. - * For example, `>1.2.3 <4.5.6` matches versions that are `greater than 1.2.3 AND less than 4.5.6`. - * - * The (intersection) set of comparators can combined by ` || ` (spaces are required) to form the - * union set of the intersection sets. So the semantic selector is in disjunctive normal form. - * - * Metadata and pre-release of VersionNumber are ignored. - * So `1.0.0` matches any versions that have `1.0.0` as normal version with any pre-release version - * or any metadata like `1.0.0-alpha`, `1.0.0+metadata`. - * - * Wildcard (`x`, `X`, `*`) can be used to match any number of minor or patch version. - * Actually, `1.0.x` is equivalent to `=1.0` (that is equivalent to `>=1.0.0 <1.1.0`) - * - * The hyphen range like `1.2.3 - 4.5.6` matches inclusive set of versions. - * So `1.2.3 - 4.5.6` is equivalent to `>=1.2.3 <=4.5.6`. - * Both sides of comparators around - are required and they can not have any operators. - * For example, `>=1.2.3 - 4.5.6` is invalid. - * - * @param selector A string that represents semantic version selector. - * @return A `SemanticSelector` that can match only onto specific semantic versions. - * @throws java.lang.IllegalAccessException when selector is in invalid format of semantic version selector. - */ - def apply(selector: String): SemanticSelector = { - val orChunkTokens = selector.split("\\s+\\|\\|\\s+").map(_.trim) - val orChunks = orChunkTokens.map { chunk => - SemSelAndChunk(chunk) - } - new SemanticSelector(orChunks) {} - } - - private[this] sealed trait SemSelOperator - private[this] case object Lte extends SemSelOperator { - override def toString: String = "<=" - } - private[this] case object Lt extends SemSelOperator { - override def toString: String = "<" - } - private[this] case object Gte extends SemSelOperator { - override def toString: String = ">=" - } - private[this] case object Gt extends SemSelOperator { - override def toString: String = ">" - } - private[this] case object Eq extends SemSelOperator { - override def toString: String = "=" - } - - private[SemanticSelector] final case class SemSelAndChunk(comparators: Seq[SemComparator]) { - def matches(version: VersionNumber): Boolean = { - comparators.forall(_.matches(version)) - } - override def toString: String = comparators.map(_.toString).mkString(" ") - } - private[SemanticSelector] object SemSelAndChunk { - def apply(andClauseToken: String): SemSelAndChunk = parse(andClauseToken) - private[this] def parse(andClauseToken: String): SemSelAndChunk = { - val comparatorTokens = andClauseToken.split("\\s+") - val hyphenIndex = comparatorTokens.indexWhere(_ == "-") - val comparators = if (hyphenIndex == -1) { - comparatorTokens.map(SemComparator.apply) - } else { - // interpret `A.B.C - D.E.F` to `>=A.B.C <=D.E.F` - val (before, after) = comparatorTokens.splitAt(hyphenIndex) - (before.lastOption, after.drop(1).headOption) match { - case (Some(fromStr), Some(toStr)) => - // from and to can not have an operator. - if (hasOperator(fromStr) || hasOperator(toStr)) { - throw new IllegalArgumentException( - s"Invalid ' - ' range, both side of comparators can not have an operator: $fromStr - $toStr") - } - val from = SemComparator(fromStr) - val to = SemComparator(toStr) - val comparatorsBefore = before.dropRight(1).map(SemComparator.apply) - val comparatorsAfter = after.drop(2) match { - case tokens if !tokens.isEmpty => - parse(tokens.mkString(" ")).comparators - case _ => Seq.empty - } - from.copy(op = Gte) +: - to.copy(op = Lte) +: - (comparatorsBefore ++ comparatorsAfter) - case _ => - throw new IllegalArgumentException( - s"Invalid ' - ' range position, both side of versions must be specified: $andClauseToken") - } - } - SemSelAndChunk(comparators.flatMap(_.expandWildcard)) - } - - private[this] def hasOperator(comparator: String): Boolean = { - comparator.startsWith("<") || - comparator.startsWith(">") || - comparator.startsWith("=") - } - } - - private[SemanticSelector] final case class SemComparator private ( - op: SemSelOperator, - major: Option[Long], - minor: Option[Long], - patch: Option[Long], - tags: Seq[String] - ) { - def matches(version: VersionNumber): Boolean = { - // Fill empty fields of version specifier with 0 or max value of Long. - // By filling them, SemComparator realize the properties below - // `<=1.0` is equivalent to `<1.1.0` (`<=1.0.${Long.MaxValue}`) - // `<1.0` is equivalent to `<1.0.0` - // `>=1.0` is equivalent to `>=1.0.0` - // `>1.0` is equivalent to `>=1.1.0` (`>1.0.${Long.MaxValue}`) - // - // However this fills 0 for a comparator that have `=` operator, - // a comparator that have empty part of version and `=` operator won't appear - // because of expanding it to and clause of comparators. - val assumed = op match { - case Lte => Long.MaxValue - case Lt => 0L - case Gte => 0L - case Gt => Long.MaxValue - case Eq => 0L - } - // empty fields of the version number are assumed to be 0. - val versionNumber = - (version._1.getOrElse(0L), version._2.getOrElse(0L), version._3.getOrElse(0L)) - val selector = (major.getOrElse(assumed), minor.getOrElse(assumed), patch.getOrElse(assumed)) - val normalVersionCmp = - implicitly[Ordering[(Long, Long, Long)]].compare(versionNumber, selector) - val cmp = - if (normalVersionCmp == 0) SemComparator.comparePreReleaseTags(version.tags, tags) - else normalVersionCmp - op match { - case Lte if cmp <= 0 => true - case Lt if cmp < 0 => true - case Gte if cmp >= 0 => true - case Gt if cmp > 0 => true - case Eq if cmp == 0 => true - case _ => false - } - } - - // Expand wildcard with `=` operator to and clause of comparators. - // `=1.0` is equivalent to `>=1.0 <=1.0` - def expandWildcard: Seq[SemComparator] = { - if (op == Eq && !allFieldsSpecified) { - Seq(this.copy(op = Gte), this.copy(op = Lte)) - } else { - Seq(this) - } - } - private[this] def allFieldsSpecified: Boolean = - major.isDefined && minor.isDefined && patch.isDefined - - override def toString: String = { - val versionStr = Seq(major, minor, patch) - .collect { - case Some(v) => v.toString - } - .mkString(".") - val tagsStr = if (tags.nonEmpty) s"-${tags.mkString("-")}" else "" - s"$op$versionStr$tagsStr" - } - } - private[SemanticSelector] object SemComparator { - def apply(comparator: String): SemComparator = parse(comparator) - private[this] val ComparatorRegex = """(?x)^ - ([<>]=?|=)? - (?:(\d+|[xX*]) - (?:\.(\d+|[xX*]) - (?:\.(\d+|[xX*]))? - )? - )((?:-\w+)*)$ - """.r - private[this] def parse(comparator: String): SemComparator = { - comparator match { - case ComparatorRegex(rawOp, rawMajor, rawMinor, rawPatch, ts) => - val opStr = Option(rawOp) - val major = Option(rawMajor) - val minor = Option(rawMinor) - val patch = Option(rawPatch) - val tags = splitDash(ts) - - // Trim wildcard(x, X, *) and re-parse it. - // By trimming it, comparator realize the property like - // `=1.2.x` is equivalent to `=1.2`. - val hasXrangeSelector = Seq(major, minor, patch).exists { - case Some(str) => str.matches("[xX*]") - case None => false - } - if (hasXrangeSelector) { - if (tags.nonEmpty) - throw new IllegalArgumentException( - s"Pre-release version requires major, minor, patch versions to be specified: $comparator") - val numbers = Seq(major, minor, patch).takeWhile { - case Some(str) => str.matches("\\d+") - case None => false - } - parse( - numbers - .collect { - case Some(v) => v.toString - } - .mkString(".") - ) - } else { - if (tags.nonEmpty && (major.isEmpty || minor.isEmpty || patch.isEmpty)) - throw new IllegalArgumentException( - s"Pre-release version requires major, minor, patch versions to be specified: $comparator") - val operator = opStr match { - case Some("<") => Lt - case Some("<=") => Lte - case Some(">") => Gt - case Some(">=") => Gte - case Some("=") => Eq - case None => Eq - case Some(_) => - throw new IllegalArgumentException(s"Invalid operator: $opStr") - } - SemComparator( - operator, - major.map(_.toLong), - minor.map(_.toLong), - patch.map(_.toLong), - tags - ) - } - case _ => throw new IllegalArgumentException(s"Invalid comparator: $comparator") - } - } - private[this] def splitOn[A](s: String, sep: Char): Vector[String] = - if (s eq null) Vector() - else s.split(sep).filterNot(_ == "").toVector - private[this] def splitDash(s: String) = splitOn(s, '-') - - private[SemComparator] def comparePreReleaseTags(ts1: Seq[String], ts2: Seq[String]): Int = { - // > When major, minor, and patch are equal, a pre-release version has lower precedence than a normal version. - if (ts1.isEmpty && ts2.isEmpty) 0 - else if (ts1.nonEmpty && ts2.isEmpty) -1 // ts1 is pre-release version - else if (ts1.isEmpty && ts2.nonEmpty) 1 // ts2 is pre-release version - else compareTags(ts1, ts2) - } - - @tailrec - private[this] def compareTags(ts1: Seq[String], ts2: Seq[String]): Int = { - // > A larger set of pre-release fields has a higher precedence than a smaller set, - // > if all of the preceding identifiers are equal. - if (ts1.isEmpty && ts2.isEmpty) 0 - else if (ts1.nonEmpty && ts2.isEmpty) 1 - else if (ts1.isEmpty && ts2.nonEmpty) -1 - else { - val ts1head = ts1.head - val ts2head = ts2.head - val cmp = (ts1head.matches("\\d+"), ts2head.matches("\\d+")) match { - // Identifiers consisting of only digits are compared numerically. - // Numeric identifiers always have lower precedence than non-numeric identifiers. - // Identifiers with letters are compared case insensitive lexical order. - case (true, true) => implicitly[Ordering[Long]].compare(ts1head.toLong, ts2head.toLong) - case (false, true) => 1 - case (true, false) => -1 - case (false, false) => ts1head.toLowerCase.compareTo(ts2head.toLowerCase) - } - if (cmp == 0) compareTags(ts1.tail, ts2.tail) - else cmp - } - } - } -} From d5f5cbb0610ef4621fad675574471c7176ca22e7 Mon Sep 17 00:00:00 2001 From: tanishiking24 Date: Sat, 12 May 2018 12:26:44 +0900 Subject: [PATCH 4/4] Rename VersionNumber#satisfies to VersionNumber#matchesSemVer VersionNumber#matchesSemVer receive SemanticSelector instead of String --- core/src/main/scala/sbt/librarymanagement/VersionNumber.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/sbt/librarymanagement/VersionNumber.scala b/core/src/main/scala/sbt/librarymanagement/VersionNumber.scala index 102d468c3..2437466cc 100644 --- a/core/src/main/scala/sbt/librarymanagement/VersionNumber.scala +++ b/core/src/main/scala/sbt/librarymanagement/VersionNumber.scala @@ -27,8 +27,8 @@ final class VersionNumber private[sbt] ( case _ => false } - def satisfies(selector: String): Boolean = { - SemanticSelector(selector).matches(this) + def matchesSemVer(selsem: SemanticSelector): Boolean = { + selsem.matches(this) } /** A variant of mkString that returns the empty string if the sequence is empty. */