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

Issue:
Steward updates different artifacts with same version number
See https://github.com/playframework/play-ws/pull/461/files#diff-0:

![Screenshot 2020-02-11 at 12 05 46](https://user-images.githubusercontent.com/762126/74231435-db9daa00-4cc6-11ea-886b-4392a56f7412.png)

The steward was updating `scalafmt` from `2.0.1` to `2.0.7` and (incidentally) `sbt-pgp` got updated from `2.0.1` to `2.0.7` (which doesn't exist) too.

Pull request:
This only allows the `moduleId` heuristic to modify version numbers in
ModuleID definitions. It prevents that version numbers are bumped in
unrelated ModuleIDs.

Closes #1314.

Patch:
diff --git a/modules/core/src/main/scala/org/scalasteward/core/edit/UpdateHeuristic.scala b/modules/core/src/main/scala/org/scalasteward/core/edit/UpdateHeuristic.scala
index 3b2cd3616b..35dd2c2c89 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/edit/UpdateHeuristic.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/edit/UpdateHeuristic.scala
@@ -52,6 +52,12 @@ object UpdateHeuristic {
   private def shouldBeIgnored(prefix: String): Boolean =
     prefix.toLowerCase.contains("previous") || prefix.trim.startsWith("//")
 
+  private def moduleIdRegex(version: String): Regex = {
+    val ident = """[^":\n]+"""
+    val v = Regex.quote(version)
+    raw"""(.*)(["|`]($ident)(?:"\s*%+\s*"|:+)($ident)(?:"\s*%\s*|:+))("?)($v)("|`)""".r
+  }
+
   private def replaceArtifactF(update: Update): String => Option[String] = { target =>
     update match {
       case s @ Update.Single(_, _, Some(newerGroupId), Some(newerArtifactId)) =>
@@ -85,28 +91,27 @@ object UpdateHeuristic {
         s"(?i)(.*)($prefix$searchTerms.*?)$currentVersion(.?)".r
       }
 
-    def replaceF(update: Update): String => Option[String] =
-      target => replaceVersionF(update)(target) >>= replaceArtifactF(update)
-
     def replaceVersionF(update: Update): String => Option[String] =
       mkRegex(update).fold((_: String) => Option.empty[String]) { regex => target =>
         replaceSomeInAllowedParts(
           regex,
           target,
           match0 => {
-            val group1 = match0.group(1)
-            val group2 = match0.group(2)
-            val lastGroup = match0.group(match0.groupCount)
-            if (
-              shouldBeIgnored(group1) ||
-              !enclosingCharsDelimitVersion(group2.lastOption, lastGroup.headOption)
-            ) None
-            else Some(Regex.quoteReplacement(group1 + group2 + update.nextVersion + lastGroup))
+            val prefix = match0.group(1)
+            val dependency = match0.group(2)
+            val versionSuffix = match0.group(match0.groupCount)
+            Option.when {
+              !shouldBeIgnored(prefix) &&
+              enclosingCharsDelimitVersion(dependency.lastOption, versionSuffix.headOption) &&
+              !moduleIdRegex(update.currentVersion).matches(match0.matched)
+            } {
+              Regex.quoteReplacement(prefix + dependency + update.nextVersion + versionSuffix)
+            }
           }
         ).someIfChanged
       }
 
-    replaceF
+    update => target => replaceVersionF(update)(target) >>= replaceArtifactF(update)
   }
 
   private def enclosingCharsDelimitVersion(before: Option[Char], after: Option[Char]): Boolean =
@@ -132,67 +137,63 @@ object UpdateHeuristic {
   private def isCommonWord(s: String): Boolean =
     s === "scala"
 
-  val moduleId = UpdateHeuristic(
+  val moduleId: UpdateHeuristic = UpdateHeuristic(
     name = "moduleId",
     replaceVersion = update =>
       target =>
-        {
-          val groupId = Regex.quote(update.groupId.value)
-          val artifactIds =
-            alternation(update.artifactIds.map(artifactId => Regex.quote(artifactId.name)))
-          val currentVersion = Regex.quote(update.currentVersion)
-          val regex =
-            raw"""(.*)(["|`]$groupId(?:"\s*%+\s*"|:+)$artifactIds(?:"\s*%\s*|:+))("?)($currentVersion)("|`)""".r
-          replaceSomeInAllowedParts(
-            regex,
-            target,
-            match0 => {
-              val precedingCharacters = match0.group(1)
-              val dependency = match0.group(2)
-              val versionPrefix = match0.group(4)
-              val versionSuffix = match0.group(6)
-              if (shouldBeIgnored(precedingCharacters)) None
-              else
-                Some(
-                  Regex.quoteReplacement(
-                    s"""$precedingCharacters$dependency$versionPrefix${update.nextVersion}$versionSuffix"""
-                  )
-                )
+        replaceSomeInAllowedParts(
+          moduleIdRegex(update.currentVersion),
+          target,
+          match0 => {
+            val prefix = match0.group(1)
+            val dependency = match0.group(2)
+            val groupId = match0.group(3)
+            val artifactId = match0.group(4)
+            val versionPrefix = match0.group(5)
+            val versionSuffix = match0.group(7)
+            Option.when {
+              !shouldBeIgnored(prefix) &&
+              update.groupId.value === groupId &&
+              update.artifactIds.exists(_.name === artifactId)
+            } {
+              Regex.quoteReplacement(
+                s"""$prefix$dependency$versionPrefix${update.nextVersion}$versionSuffix"""
+              )
             }
-          ).someIfChanged
-        } >>= replaceArtifactF(update)
+          }
+        ).someIfChanged >>= replaceArtifactF(update)
   )
 
-  val strict = UpdateHeuristic(
+  val strict: UpdateHeuristic = UpdateHeuristic(
     name = "strict",
     replaceVersion = defaultReplaceVersion(searchTerms, update => Some(s"${update.groupId}.*?"))
   )
 
-  val original = UpdateHeuristic(
+  val original: UpdateHeuristic = UpdateHeuristic(
     name = "original",
     replaceVersion = defaultReplaceVersion(searchTerms)
   )
 
-  val relaxed = UpdateHeuristic(
+  val relaxed: UpdateHeuristic = UpdateHeuristic(
     name = "relaxed",
     replaceVersion = defaultReplaceVersion { update =>
       util.string.extractWords(update.mainArtifactId).filterNot(isCommonWord)
     }
   )
 
-  val sliding = UpdateHeuristic(
+  val sliding: UpdateHeuristic = UpdateHeuristic(
     name = "sliding",
     replaceVersion = defaultReplaceVersion(
       _.mainArtifactId.toSeq.sliding(5).map(_.unwrap).filterNot(isCommonWord).toList
     )
   )
 
-  val completeGroupId = UpdateHeuristic(
+  val completeGroupId: UpdateHeuristic = UpdateHeuristic(
     name = "completeGroupId",
     replaceVersion = defaultReplaceVersion(update => List(update.groupId.value))
   )
 
-  val groupId = UpdateHeuristic(
+  val groupId: UpdateHeuristic = UpdateHeuristic(
     name = "groupId",
     replaceVersion = defaultReplaceVersion(
       _.groupId.value
@@ -204,7 +205,7 @@ object UpdateHeuristic {
     )
   )
 
-  val specific = UpdateHeuristic(
+  val specific: UpdateHeuristic = UpdateHeuristic(
     name = "specific",
     replaceVersion = defaultReplaceVersion {
       case s: Update.Single


Test patch:
diff --git a/modules/core/src/test/scala/org/scalasteward/core/edit/EditAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/edit/EditAlgTest.scala
index 41e620bb29..5617c056c0 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/edit/EditAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/edit/EditAlgTest.scala
@@ -149,15 +149,15 @@ class EditAlgTest extends FunSuite {
     assertEquals(state, expected)
   }
 
-  test("reproduce https://github.com/circe/circe-config/pull/40") {
+  test("https://github.com/circe/circe-config/pull/40") {
     val update = Update.Single("com.typesafe" % "config" % "1.3.3", Nel.of("1.3.4"))
     val original = Map(
       "build.sbt" -> """val config = "1.3.3"""",
       "project/plugins.sbt" -> """addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "1.3.3")"""
     )
     val expected = Map(
-      "build.sbt" -> """val config = "1.3.3"""", // the version should have been updated here
-      "project/plugins.sbt" -> """addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "1.3.4")"""
+      "build.sbt" -> """val config = "1.3.4"""",
+      "project/plugins.sbt" -> """addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "1.3.3")"""
     )
     assertEquals(runApplyUpdate(update, original), expected)
   }
diff --git a/modules/core/src/test/scala/org/scalasteward/core/edit/UpdateHeuristicTest.scala b/modules/core/src/test/scala/org/scalasteward/core/edit/UpdateHeuristicTest.scala
index 4ca4dc83e3..89dad75a9c 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/edit/UpdateHeuristicTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/edit/UpdateHeuristicTest.scala
@@ -457,16 +457,24 @@ class UpdateHeuristicTest extends FunSuite {
     )
   }
 
-  test("NOK: change of unrelated ModuleID") {
+  test("unrelated ModuleID with same version number") {
     val original = """ "com.geirsson" % "sbt-ci-release" % "1.2.1" """
-    val expected = """ "com.geirsson" % "sbt-ci-release" % "1.2.4" """
-    assertEquals(
-      Group(
-        "org.scala-sbt" % Nel.of("sbt-launch", "scripted-plugin", "scripted-sbt") % "1.2.1",
-        Nel.of("1.2.4")
-      ).replaceVersionIn(original),
-      Some(expected) -> UpdateHeuristic.relaxed.name
+    val update = Group(
+      "org.scala-sbt" % Nel.of("sbt-launch", "scripted-plugin", "scripted-sbt") % "1.2.1",
+      Nel.of("1.2.4")
     )
+    assertEquals(update.replaceVersionIn(original), None -> UpdateHeuristic.all.last.name)
+  }
+
+  test("issue 1314: unrelated ModuleID with same version number, 2") {
+    val original = """val scalafmt = "2.0.1"
+                     |addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.0.1")
+                     |""".stripMargin
+    val expected = """val scalafmt = "2.0.7"
+                     |addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.0.1")
+                     |""".stripMargin
+    val update = Single("org.scalameta" % "sbt-scalafmt" % "2.0.1", Nel.of("2.0.7"))
+    assertEquals(update.replaceVersionIn(original), Some(expected) -> UpdateHeuristic.relaxed.name)
   }
 
   test("disable updates on single lines with `off` (no `on`)") {
@@ -588,7 +596,7 @@ class UpdateHeuristicTest extends FunSuite {
     )
   }
 
-  test("issue 1586 - tracing value for opentracing library") {
+  test("issue 1586: tracing value for opentracing library") {
     val original = """val tracing = "2.4.1" """
     val expected = """val tracing = "2.5.0" """
     assertEquals(
@@ -644,7 +652,7 @@ class UpdateHeuristicTest extends FunSuite {
     )
   }
 
-  test("issue #1651: don't update in comments") {
+  test("issue 1651: don't update in comments") {
     val original =
       """val scalaTest = "3.2.0"  // scalaTest 3.2.0-M2 is causing a failure on scala 2.13..."""
     val expected =