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/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*") + ), ) diff --git a/sbt-version-policy/src/main/scala/sbtversionpolicy/DependencyCheckReport.scala b/sbt-version-policy/src/main/scala/sbtversionpolicy/DependencyCheckReport.scala index e94fc00..dd1993e 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" } + private case 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,24 +155,38 @@ 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))) => - 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) { + // 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 = (samePatch && sameSuffix) || (previousSemVer.suffix.isEmpty || previousSemVer.patch > 0) + + sameMajor && sameMinor && compatPatch } - case None => currentVersion == previousVersion + case _ => false } } - private def extractSemVerNumbers(versionString: String): Option[(Int, Int, Int)] = { + private def extractSemVerNumbers(versionString: String): Option[SemVerVersion] = { 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, suffix @ _*) => + 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 642d030..29b4f91 100644 --- a/sbt-version-policy/src/test/scala/sbtversionpolicy/DependencyCheckReportTest.scala +++ b/sbt-version-policy/src/test/scala/sbtversionpolicy/DependencyCheckReportTest.scala @@ -20,20 +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") - 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.0.0", "1.0.0-RC1") - isCompatible("1.0.0-RC1", "1.0.0-RC1") + 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 ("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") } }