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

Issue:
Version.Component.parse parses githash as separate components
Hi, I hit an interesting case recently which I believe boils down a possible issue in Version.Component.parse (hence the title).

Say I have the following versions:

```scala
val currentVersion = Version("1.0.0+1319.ae77058")
val nextVersion = Version("1.0.0+1320.38b57aa")
```

The version suffix consists of a monotonically increasing build number and a githash.

I would expect `currentVersion.selectNext(List(nextVersion)` to return `Some(nextVersion)`.

However, it actually returns `None`.

When I looked into this more, it turns out that this is returning `None` because of how nextVersion is parsed:

```scala
Version.Component.parse("1.0.0+1320.38b57aa").foreach(println(_))
```

returns:

```
Numeric(1)
Separator(.)
Numeric(0)
Separator(.)
Numeric(0)
Separator(+)
Numeric(1320)
Separator(.)
Numeric(38)
Alpha(b)
Numeric(57)
Alpha(aa)
```

Crucially, the segment `.38b57aa` is parsed into five components: `Separator(.), Numeric(38), Alpha(b), Numeric(57), Alpha(aa)`

Calling `Alpha(b).order` returns -4, because this component is getting interpreted as a "beta" component, at this line: https://github.com/scala-steward-org/scala-steward/blob/main/modules/core/src/main/scala/org/scalasteward/core/data/Version.scala#L171C5-L171C5

So then the `nextVersion.minAlphaOrder` returns -4, causing it to get filtered out at this line: https://github.com/scala-steward-org/scala-steward/blob/main/modules/core/src/main/scala/org/scalasteward/core/data/Version.scala#L70

This turns out to be the case for any first containing substrings `m`, `ea`, `rc`, etc.

For example, this ends up printing None:

```scala
import org.scalasteward.core.data.Version

val currentVersion = Version("1.0.0+1319.ae77058")
val nextVersions = List(
  Version("1.0.0+1320.38b57aa"),
  Version("1.0.0+1320.38m57aa"),
  Version("1.0.0+1320.38ea57aa")
)

println(currentVersion.selectNext(nextVersions)) // None
```

Some thoughts/observations:

I would have expected "38m57aa" to get parsed as a component, not a set of components.

It seems like scala-steward already knows that the dot character is a separator, it just doesn't know that "38m57aa" is a githash. 

The component parser does have a sub-parser for "hash", but it's unclear whether this particular substring matches that pattern. I'm not familiar with the cats parser syntax, so I can't really interpret whether this is desired or a defect.

Even if this particular case was parsed "correctly" (by my interpretation), what happens when the githash component starts with a b or m or ea?

Pull request:
This allows hashes to also be separated with a `.` and `_` from the preceding component.

Here is for example what `Version.Component.parse("1.0.0+1320.38b57aa")` returned before this change and what it returns now:
```
before:
Numeric(1), Separator(.), Numeric(0), Separator(.), Numeric(0), Separator(+),
Numeric(1320), Separator(.), Numeric(38), Alpha(b), Numeric(57), Alpha(aa)

now:
Numeric(1), Separator(.), Numeric(0), Separator(.), Numeric(0), Separator(+),
Numeric(1320), Separator(.), Hash(38b57aa))
```

Recognizing hashes was added in #1549 but that PR gives no explanation why they only could preceded by `-` and `+`.

Closes #3124

Patch:
diff --git a/modules/core/src/main/scala/org/scalasteward/core/data/Version.scala b/modules/core/src/main/scala/org/scalasteward/core/data/Version.scala
index 9bb595ae14..22c005d6a0 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/data/Version.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/data/Version.scala
@@ -190,7 +190,7 @@ object Version {
       val numeric = Numbers.digits.map(s => List(Numeric(s)))
       val alpha = Parser.charsWhile(c => !digits(c) && !separators(c)).map(s => List(Alpha(s)))
       val separator = Parser.charIn(separators).map(c => List(Separator(c)))
-      val hash = (Parser.charIn('-', '+') ~
+      val hash = (Parser.charIn(separators) ~
         Parser.char('g').string.? ~
         Rfc5234.hexdig.rep(6).string.filterNot(startsWithDate)).backtrack
         .map { case ((s, g), h) => List(Separator(s), Hash(g.getOrElse("") + h)) }


Test patch:
diff --git a/modules/core/src/test/scala/org/scalasteward/core/data/VersionTest.scala b/modules/core/src/test/scala/org/scalasteward/core/data/VersionTest.scala
index 505bbdd330..72dbec4832 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/data/VersionTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/data/VersionTest.scala
@@ -222,6 +222,7 @@ class VersionTest extends DisciplineSuite {
       ("10000000", List("20000000"), Some("20000000")),
       ("1032048a", List("2032048a4c2"), Some("2032048a4c2")),
       ("0.1.1-3dfde9d7", List("0.2.1-485fdf3b"), None),
+      ("1.0.0+1319.ae77058", List("1.0.0+1320.38b57aa"), Some("1.0.0+1320.38b57aa")),
       ("0.1.1", List("0.2.1-485fdf3b"), None),
       ("0.1.1-ALPHA", List("0.2.1-485fdf3b"), None),
       ("0.1.1-ALPHA", List("0.2.1-BETA"), None),