Repository: scala-steward-org/scala-steward (scala)

Issue:
Avoid pushing scala updates for non LTS versions
It would be very helpful if updates for the scala language were limited to the LTS versions unless otherwise configured. The Steward pushed out versions for 3.4 recently and this version is not intended for libraries.

Pull request description:
Scala Steward now distinguishes between Scala Next and Scala LTS.

When the current Scala 3 versions is below 3.4.0 no updates to a newer Scala Next version will be created. But updates to newer Scala 3.3.x version will be created.

If the current version is already a Scala Next version (3.4.x), updates to newer Scala Next versions will be created.

In the future the `versionLine` property can be used to identify LTS and Next version. (https://github.com/scala/scala3/pull/19986)

fixes: #3302
followup to: #3323

I am bit unsure if I should release the changes  to `default.scala-steward.conf` later, after cutting a new release and a bit of wait time, as otherwise older versions would again create pull-requests for Scala 3.4.1. But that probably has already happened in most cases.

Patch:
diff --git a/modules/core/src/main/scala/org/scalasteward/core/data/package.scala b/modules/core/src/main/scala/org/scalasteward/core/data/package.scala
index 1c00fc1dba..57c5dde690 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/data/package.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/data/package.scala
@@ -19,14 +19,23 @@ package org.scalasteward.core
 package object data {
   val scalaLangGroupId: GroupId = GroupId("org.scala-lang")
 
-  val scalaLangModules: List[(GroupId, ArtifactId)] =
+  val scala2LangModules: List[(GroupId, ArtifactId)] =
     List(
       (scalaLangGroupId, ArtifactId("scala-compiler")),
       (scalaLangGroupId, ArtifactId("scala-library")),
       (scalaLangGroupId, ArtifactId("scala-reflect")),
-      (scalaLangGroupId, ArtifactId("scalap")),
+      (scalaLangGroupId, ArtifactId("scalap"))
+    )
+
+  val scala3LangModules: List[(GroupId, ArtifactId)] =
+    List(
       (scalaLangGroupId, ArtifactId("scala3-compiler")),
       (scalaLangGroupId, ArtifactId("scala3-library")),
       (scalaLangGroupId, ArtifactId("scala3-library_sjs1"))
     )
+
+  val scalaLangModules: List[(GroupId, ArtifactId)] =
+    scala2LangModules ++ scala3LangModules
+
+  val scalaNextMinVersion: Version = Version("3.4.0")
 }
diff --git a/modules/core/src/main/scala/org/scalasteward/core/update/FilterAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/update/FilterAlg.scala
index 8d19778280..1440fef1ec 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/update/FilterAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/update/FilterAlg.scala
@@ -54,6 +54,7 @@ object FilterAlg {
         case NotAllowedByConfig(_)      => "not allowed by config"
         case NoSuitableNextVersion(_)   => "no suitable next version"
         case VersionOrderingConflict(_) => "version ordering conflict"
+        case IgnoreScalaNext(_)         => "not upgrading from Scala LTS to Next version"
       }
   }
 
@@ -62,9 +63,30 @@ object FilterAlg {
   final case class NotAllowedByConfig(update: Update.ForArtifactId) extends RejectionReason
   final case class NoSuitableNextVersion(update: Update.ForArtifactId) extends RejectionReason
   final case class VersionOrderingConflict(update: Update.ForArtifactId) extends RejectionReason
+  final case class IgnoreScalaNext(update: Update.ForArtifactId) extends RejectionReason
 
   def localFilter(update: Update.ForArtifactId, repoConfig: RepoConfig): FilterResult =
-    repoConfig.updates.keep(update).flatMap(globalFilter(_, repoConfig))
+    repoConfig.updates.keep(update).flatMap(scalaLTSFilter).flatMap(globalFilter(_, repoConfig))
+
+  def scalaLTSFilter(update: Update.ForArtifactId): FilterResult =
+    if (!isScala3Lang(update))
+      Right(update)
+    else {
+      if (update.currentVersion >= scalaNextMinVersion) {
+        // already on Scala Next
+        Right(update)
+      } else {
+        val filteredVersions = update.newerVersions.filterNot(_ >= scalaNextMinVersion)
+        if (filteredVersions.nonEmpty)
+          Right(update.copy(newerVersions = Nel.fromListUnsafe(filteredVersions)))
+        else Left(IgnoreScalaNext(update))
+      }
+    }
+
+  def isScala3Lang(update: Update.ForArtifactId): Boolean =
+    scala3LangModules.exists { case (g, a) =>
+      update.groupId == g && update.artifactIds.exists(_ == a)
+    }
 
   private def globalFilter(update: Update.ForArtifactId, repoConfig: RepoConfig): FilterResult =
     selectSuitableNextVersion(update, repoConfig).flatMap(checkVersionOrdering)


Test patch:
diff --git a/modules/core/src/test/scala/org/scalasteward/core/update/FilterAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/update/FilterAlgTest.scala
index 5acc4df380..b1bd71da65 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/update/FilterAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/update/FilterAlgTest.scala
@@ -246,4 +246,30 @@ class FilterAlgTest extends FunSuite {
     val dependency = "org.typelevel".g % ("cats-effect", "cats-effect_2.12").a % "1.0.0"
     assert(isDependencyConfigurationIgnored(dependency.copy(configurations = Some("scalafmt"))))
   }
+
+  test("scalaLTSFilter: LTS, no update") {
+    val update = ("org.scala-lang".g % "scala3-compiler".a % "3.3.2" %> Nel.of("3.4.0")).single
+    assertEquals(scalaLTSFilter(update), Left(IgnoreScalaNext(update)))
+  }
+
+  test("scalaLTSFilter: LTS, filter versions") {
+    val update =
+      ("org.scala-lang".g % "scala3-compiler".a % "3.3.2" %> Nel.of("3.3.3", "3.4.0")).single
+    assertEquals(scalaLTSFilter(update), Right(update.copy(newerVersions = Nel.of("3.3.3".v))))
+  }
+
+  test("scalaLTSFilter: Next") {
+    val update = ("org.scala-lang".g % "scala3-compiler".a % "3.4.0" %> Nel.of("3.4.1")).single
+    assertEquals(scalaLTSFilter(update), Right(update))
+  }
+
+  test("isScala3Lang: true") {
+    val update = ("org.scala-lang".g % "scala3-compiler".a % "3.3.3" %> Nel.of("3.4.0")).single
+    assert(isScala3Lang(update))
+  }
+
+  test("isScala3Lang: false") {
+    val update = ("org.scala-lang".g % "scala-compiler".a % "2.13.11" %> Nel.of("2.13.12")).single
+    assert(!isScala3Lang(update))
+  }
 }