From 3738510977b6f14db7ded9f1818ed19ca2414aa5 Mon Sep 17 00:00:00 2001 From: Alexey Shuksto Date: Fri, 28 Jul 2023 19:35:14 +0300 Subject: [PATCH 1/5] fix scalacenter/sbt-version-policy#155 by extracting sem. version without suffix --- .gitignore | 8 ++++++++ .../scala/sbtversionpolicy/DependencyCheckReport.scala | 10 ++++++---- .../sbtversionpolicy/DependencyCheckReportTest.scala | 5 ++++- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 2f7896d..b4d0b12 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,9 @@ +# SBT stuff target/ + +# Bloop/Metals/VSCode stuff +.bloop +.bsp +.metals +.vscode +metals.sbt diff --git a/sbt-version-policy/src/main/scala/sbtversionpolicy/DependencyCheckReport.scala b/sbt-version-policy/src/main/scala/sbtversionpolicy/DependencyCheckReport.scala index e94fc00..222ef46 100644 --- a/sbt-version-policy/src/main/scala/sbtversionpolicy/DependencyCheckReport.scala +++ b/sbt-version-policy/src/main/scala/sbtversionpolicy/DependencyCheckReport.scala @@ -167,10 +167,12 @@ object DependencyCheckReport { private def extractSemVerNumbers(versionString: String): Option[(Int, Int, Int)] = { val version = Version(versionString) - if (version.items.size == 3 && version.items.forall(_.isInstanceOf[Version.Number])) { - val Seq(major, minor, patch) = version.items.collect { case num: Version.Number => num.value } - Some((major, minor, patch)) - } else None // Not a normalized version number (e.g., 1.0.0-RC1) + version.items match { + case Vector(major: Version.Number, minor: Version.Number, patch: Version.Number, _*) => + Some((major.value, minor.value, patch.value)) + case _ => + None // Not a semantic version number (e.g., 1.0-RC1) + } } } diff --git a/sbt-version-policy/src/test/scala/sbtversionpolicy/DependencyCheckReportTest.scala b/sbt-version-policy/src/test/scala/sbtversionpolicy/DependencyCheckReportTest.scala index 642d030..498a0f6 100644 --- a/sbt-version-policy/src/test/scala/sbtversionpolicy/DependencyCheckReportTest.scala +++ b/sbt-version-policy/src/test/scala/sbtversionpolicy/DependencyCheckReportTest.scala @@ -32,8 +32,11 @@ object DependencyCheckReportTest extends BasicTestSuite { isCompatible("1.0.1", "1.0.0") isBreaking ("1.1.0", "1.0.0") isBreaking ("2.0.0", "1.0.0") - isBreaking ("1.0.0", "1.0.0-RC1") + isBreaking ("1.1.0", "1.0.0-RC1") + isBreaking ("2.0.0", "1.0.0-RC1") isCompatible("1.0.0-RC1", "1.0.0-RC1") + isCompatible("1.0.0", "1.0.0-RC1") + isCompatible("1.0.1", "1.0.0-RC1") } } From 7f62e6557fedbe481b8593701e110e14250cb112 Mon Sep 17 00:00:00 2001 From: Alexey Shuksto Date: Wed, 2 Aug 2023 12:19:13 +0300 Subject: [PATCH 2/5] ignore sem.version suffix only for current version in source compat check --- .../DependencyCheckReport.scala | 12 +++++------ .../DependencyCheckReportTest.scala | 20 +++++++++---------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/sbt-version-policy/src/main/scala/sbtversionpolicy/DependencyCheckReport.scala b/sbt-version-policy/src/main/scala/sbtversionpolicy/DependencyCheckReport.scala index 222ef46..2e1131f 100644 --- a/sbt-version-policy/src/main/scala/sbtversionpolicy/DependencyCheckReport.scala +++ b/sbt-version-policy/src/main/scala/sbtversionpolicy/DependencyCheckReport.scala @@ -153,23 +153,23 @@ object DependencyCheckReport { previousVersion == currentVersion case VersionCompatibility.SemVer | VersionCompatibility.EarlySemVer | VersionCompatibility.SemVerSpec => // Early SemVer and SemVer Spec are equivalent regarding source compatibility - extractSemVerNumbers(currentVersion).zip(extractSemVerNumbers(previousVersion)).headOption match { - case Some(((currentMajor, currentMinor, currentPatch), (previousMajor, previousMinor, previousPatch))) => + (extractSemVerNumbers(currentVersion, ignoreSuffix = true), extractSemVerNumbers(previousVersion, ignoreSuffix = false)) match { + case (Some((currentMajor, currentMinor, currentPatch)), Some((previousMajor, previousMinor, previousPatch))) => currentMajor == previousMajor && { if (currentMajor == 0) currentMinor == previousMinor && currentPatch == previousPatch else currentMinor == previousMinor && currentPatch >= previousPatch } - case None => currentVersion == previousVersion + case _ => currentVersion == previousVersion } } - private def extractSemVerNumbers(versionString: String): Option[(Int, Int, Int)] = { + private def extractSemVerNumbers(versionString: String, ignoreSuffix: Boolean): Option[(Int, Int, Int)] = { val version = Version(versionString) version.items match { - case Vector(major: Version.Number, minor: Version.Number, patch: Version.Number, _*) => - Some((major.value, minor.value, patch.value)) + case Vector(major: Version.Number, minor: Version.Number, patch: Version.Number, suffix @ _*) => + Some((major.value, minor.value, patch.value)).filter(_ => ignoreSuffix || suffix.isEmpty) case _ => None // Not a semantic version number (e.g., 1.0-RC1) } diff --git a/sbt-version-policy/src/test/scala/sbtversionpolicy/DependencyCheckReportTest.scala b/sbt-version-policy/src/test/scala/sbtversionpolicy/DependencyCheckReportTest.scala index 498a0f6..3e0da4a 100644 --- a/sbt-version-policy/src/test/scala/sbtversionpolicy/DependencyCheckReportTest.scala +++ b/sbt-version-policy/src/test/scala/sbtversionpolicy/DependencyCheckReportTest.scala @@ -26,17 +26,15 @@ object DependencyCheckReportTest extends BasicTestSuite { isCompatible("1.2.3-RC1", "1.2.3-RC1") } withScheme(VersionCompatibility.EarlySemVer) { (isCompatible, isBreaking) => - isBreaking ("0.1.1", "0.1.0") - isBreaking ("0.2.0", "0.1.0") - isBreaking ("1.0.0", "0.1.0") - isCompatible("1.0.1", "1.0.0") - isBreaking ("1.1.0", "1.0.0") - isBreaking ("2.0.0", "1.0.0") - isBreaking ("1.1.0", "1.0.0-RC1") - isBreaking ("2.0.0", "1.0.0-RC1") - isCompatible("1.0.0-RC1", "1.0.0-RC1") - isCompatible("1.0.0", "1.0.0-RC1") - isCompatible("1.0.1", "1.0.0-RC1") + isBreaking ("0.1.1", "0.1.0") + isBreaking ("0.2.0", "0.1.0") + isBreaking ("1.0.0", "1.0.0-RC1") + isBreaking ("1.0.1", "1.0.0-RC1") + isBreaking ("1.1.0", "1.0.0-RC1") + isBreaking ("2.0.0", "1.0.0-RC1") + isCompatible("1.0.0-RC1", "1.0.0-RC1") + isCompatible("1.0.1-RC1", "1.0.0") + isCompatible("1.2.1-SNAPSHOT", "1.2.0") } } From e21ee296bbc601a35f5d54c98ae0cd862b87d307 Mon Sep 17 00:00:00 2001 From: Alexey Shuksto Date: Wed, 2 Aug 2023 13:53:39 +0300 Subject: [PATCH 3/5] implement suffix checks for semver patch version changes --- .../DependencyCheckReport.scala | 33 +++++++++++++------ .../DependencyCheckReportTest.scala | 20 +++++++---- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/sbt-version-policy/src/main/scala/sbtversionpolicy/DependencyCheckReport.scala b/sbt-version-policy/src/main/scala/sbtversionpolicy/DependencyCheckReport.scala index 2e1131f..dadacfc 100644 --- a/sbt-version-policy/src/main/scala/sbtversionpolicy/DependencyCheckReport.scala +++ b/sbt-version-policy/src/main/scala/sbtversionpolicy/DependencyCheckReport.scala @@ -61,6 +61,8 @@ object DependencyCheckReport { def message = "missing dependency" } + @data class SemVerVersion(major: Int, minor: Int, patch: Int, suffix: Seq[Version.Item]) + @deprecated("This method is internal.", "1.1.0") def apply( currentModules: Map[(String, String), String], @@ -153,23 +155,34 @@ object DependencyCheckReport { previousVersion == currentVersion case VersionCompatibility.SemVer | VersionCompatibility.EarlySemVer | VersionCompatibility.SemVerSpec => // Early SemVer and SemVer Spec are equivalent regarding source compatibility - (extractSemVerNumbers(currentVersion, ignoreSuffix = true), extractSemVerNumbers(previousVersion, ignoreSuffix = false)) match { - case (Some((currentMajor, currentMinor, currentPatch)), Some((previousMajor, previousMinor, previousPatch))) => - currentMajor == previousMajor && { - if (currentMajor == 0) - currentMinor == previousMinor && currentPatch == previousPatch - else - currentMinor == previousMinor && currentPatch >= previousPatch + (extractSemVerNumbers(currentVersion), extractSemVerNumbers(previousVersion)) match { + case (Some(currentSemVer), Some(previousSemVer)) => + def sameMajor = currentSemVer.major == previousSemVer.major + def sameMinor = currentSemVer.minor == previousSemVer.minor + def samePatch = currentSemVer.patch == previousSemVer.patch + def sameSuffix = currentSemVer.suffix == previousSemVer.suffix + + if (currentSemVer.major == 0) { + sameMajor && sameMinor && samePatch && sameSuffix + } else { + // 1.0.0-RC1 may be source incompatible to 1.0.0-RC2 + // but! + // 1.0.1-RC2 must be source compatible both to 1.0.1-RC1 and 1.0.0 (w/o suffix!) + def compatPatch = + (currentSemVer.patch > 0 && samePatch) || + (currentSemVer.patch > previousSemVer.patch && previousSemVer.suffix.isEmpty) + + sameMajor && sameMinor && ((samePatch && sameSuffix) || compatPatch) } - case _ => currentVersion == previousVersion + case _ => false } } - private def extractSemVerNumbers(versionString: String, ignoreSuffix: Boolean): Option[(Int, Int, Int)] = { + private def extractSemVerNumbers(versionString: String): Option[SemVerVersion] = { val version = Version(versionString) version.items match { case Vector(major: Version.Number, minor: Version.Number, patch: Version.Number, suffix @ _*) => - Some((major.value, minor.value, patch.value)).filter(_ => ignoreSuffix || suffix.isEmpty) + Some(SemVerVersion(major.value, minor.value, patch.value, suffix)) case _ => None // Not a semantic version number (e.g., 1.0-RC1) } diff --git a/sbt-version-policy/src/test/scala/sbtversionpolicy/DependencyCheckReportTest.scala b/sbt-version-policy/src/test/scala/sbtversionpolicy/DependencyCheckReportTest.scala index 3e0da4a..29b4f91 100644 --- a/sbt-version-policy/src/test/scala/sbtversionpolicy/DependencyCheckReportTest.scala +++ b/sbt-version-policy/src/test/scala/sbtversionpolicy/DependencyCheckReportTest.scala @@ -20,21 +20,29 @@ object DependencyCheckReportTest extends BasicTestSuite { withScheme(VersionCompatibility.PackVer) { (isCompatible, isBreaking) => isBreaking ("1.0.1", "1.0.0") isBreaking ("1.1.0", "1.0.0") - isBreaking ("2.0.0", "1.0.0") - isCompatible("1.2.3", "1.2.3") - isBreaking ("1.2.3", "1.2.3-RC1") isCompatible("1.2.3-RC1", "1.2.3-RC1") + isBreaking ("1.2.3", "1.2.3-RC1") + isCompatible("1.2.3", "1.2.3") + isBreaking ("2.0.0", "1.0.0") } withScheme(VersionCompatibility.EarlySemVer) { (isCompatible, isBreaking) => isBreaking ("0.1.1", "0.1.0") isBreaking ("0.2.0", "0.1.0") + isCompatible("1.0.0-RC1", "1.0.0-RC1") + isBreaking ("1.0.0-RC2", "1.0.0-RC1") isBreaking ("1.0.0", "1.0.0-RC1") + isCompatible("1.0.1-RC1", "1.0.0") + isCompatible("1.0.1-RC2", "1.0.1-RC1") isBreaking ("1.0.1", "1.0.0-RC1") + isCompatible("1.0.1", "1.0.0") + isBreaking ("1.1.0-RC1", "1.0.0") + isBreaking ("1.1.0-RC2", "1.1.0-RC1") isBreaking ("1.1.0", "1.0.0-RC1") - isBreaking ("2.0.0", "1.0.0-RC1") - isCompatible("1.0.0-RC1", "1.0.0-RC1") - isCompatible("1.0.1-RC1", "1.0.0") + isBreaking ("1.1.0", "1.0.0") isCompatible("1.2.1-SNAPSHOT", "1.2.0") + isBreaking ("2.0.0-RC1", "1.0.0") + isBreaking ("2.0.0", "1.0.0-RC1") + isBreaking ("2.0.0", "1.0.0") } } From 52be2913cce4bec20967742f3a2adc8e245e1c56 Mon Sep 17 00:00:00 2001 From: Alexey Shuksto Date: Thu, 3 Aug 2023 13:02:23 +0300 Subject: [PATCH 4/5] simplify semver patch comparison in source compatibility check --- .../sbtversionpolicy/DependencyCheckReport.scala | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/sbt-version-policy/src/main/scala/sbtversionpolicy/DependencyCheckReport.scala b/sbt-version-policy/src/main/scala/sbtversionpolicy/DependencyCheckReport.scala index dadacfc..dd1993e 100644 --- a/sbt-version-policy/src/main/scala/sbtversionpolicy/DependencyCheckReport.scala +++ b/sbt-version-policy/src/main/scala/sbtversionpolicy/DependencyCheckReport.scala @@ -61,7 +61,7 @@ object DependencyCheckReport { def message = "missing dependency" } - @data class SemVerVersion(major: Int, minor: Int, patch: Int, suffix: Seq[Version.Item]) + private case class SemVerVersion(major: Int, minor: Int, patch: Int, suffix: Seq[Version.Item]) @deprecated("This method is internal.", "1.1.0") def apply( @@ -163,16 +163,17 @@ object DependencyCheckReport { def sameSuffix = currentSemVer.suffix == previousSemVer.suffix if (currentSemVer.major == 0) { + // Before 1.x.y release even patch changes could be source incompatible, + // this includes changes between snapshots and release candidates + sameMajor && sameMinor && samePatch && sameSuffix } else { // 1.0.0-RC1 may be source incompatible to 1.0.0-RC2 // but! // 1.0.1-RC2 must be source compatible both to 1.0.1-RC1 and 1.0.0 (w/o suffix!) - def compatPatch = - (currentSemVer.patch > 0 && samePatch) || - (currentSemVer.patch > previousSemVer.patch && previousSemVer.suffix.isEmpty) - - sameMajor && sameMinor && ((samePatch && sameSuffix) || compatPatch) + def compatPatch = (samePatch && sameSuffix) || (previousSemVer.suffix.isEmpty || previousSemVer.patch > 0) + + sameMajor && sameMinor && compatPatch } case _ => false } From a0365658a819db1b0f5bf3eb1dbf1670feb124de Mon Sep 17 00:00:00 2001 From: Alexey Shuksto Date: Thu, 3 Aug 2023 15:20:30 +0300 Subject: [PATCH 5/5] add 'sbtversionpolicy.DependencyCheckReport$SemVerVersion*' to MIMA filters --- build.sbt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 9adf44e..c0f4fd6 100644 --- a/build.sbt +++ b/build.sbt @@ -1,3 +1,4 @@ +import com.typesafe.tools.mima.core._ inThisBuild(List( organization := "ch.epfl.scala", @@ -37,5 +38,9 @@ lazy val `sbt-version-policy` = project "io.get-coursier" %% "versions" % "0.3.1", "com.eed3si9n.verify" %% "verify" % "2.0.1" % Test, ), - testFrameworks += new TestFramework("verify.runner.Framework") + testFrameworks += new TestFramework("verify.runner.Framework"), + mimaBinaryIssueFilters ++= Seq( + // this class is `private` and it's only used from `extractSemVerNumbers` method, which is private + ProblemFilters.exclude[MissingClassProblem]("sbtversionpolicy.DependencyCheckReport$SemVerVersion*") + ), )