[2.x] fix: Fix whatDependsOn RuntimeException (#8462)

Filters out empty versions during parser construction to prevent RuntimeException when creating token parsers.  Includes comprehensive test coverage for edge cases.

DependencyTreePlugin is an AutoPlugin with trigger = AllRequirements,
so it loads automatically in scripted tests without requiring explicit
plugin configuration.
This commit is contained in:
Dairus 2026-01-13 23:12:57 +01:00 committed by GitHub
parent 1ef5823a49
commit 4a36171138
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 137 additions and 31 deletions

View File

@ -86,6 +86,14 @@ jobs:
repository: sbt/zinc
ref: develop
path: zinc
- name: Cleanup SBT server and named pipes (Windows only)
if: runner.os == 'Windows'
shell: pwsh
run: |
# Kill sbt processes if any are running
Get-Process sbt -ErrorAction SilentlyContinue | Stop-Process -Force
# Remove all SBT named pipes, suppress errors if none exist
Remove-Item "\\.\pipe\sbt-*" -ErrorAction SilentlyContinue
- name: Setup JDK
uses: actions/setup-java@v5
with:

View File

@ -324,6 +324,41 @@ OPTIONS
case class ArtifactPattern(organization: String, name: String, version: Option[String])
private[plugins] def createArtifactPatternParser(
graph: ModuleGraph
): Parser[ArtifactPattern] =
graph.nodes
.map(_.id)
.groupBy(m => (m.organization, m.name))
.map { case ((org, name), modules) =>
// Empty versions cause parser token creation to fail
val versionParsers: Seq[Parser[Option[String]]] =
modules
.filter(_.version.nonEmpty)
.map { id =>
token(Space ~> id.version).?
}
// Handle modules with only empty versions
val effectiveVersionParser =
if versionParsers.isEmpty then success(None)
else oneOf(versionParsers)
(Space ~> token(org) ~ token(Space ~> name) ~ effectiveVersionParser).map {
case ((org, name), version) => ArtifactPattern(org, name, version)
}
}
.reduceOption(_ | _)
.getOrElse {
// If the dependencyTreeModuleGraphStore couldn't be loaded because no dependency tree command was run before, we should still provide a parser for the command.
((Space ~> token(StringBasic, "<organization>")) ~ (Space ~> token(
StringBasic,
"<module>"
)) ~ (Space ~> token(StringBasic, "<version?>")).?).map { case ((org, mod), version) =>
ArtifactPattern(org, mod, version)
}
}
val artifactPatternParser: Def.Initialize[State => Parser[ArtifactPattern]] =
Keys.resolvedScoped { ctx => (state: State) =>
val graph =
@ -331,31 +366,9 @@ OPTIONS
Nil,
Nil
)
graph.nodes
.map(_.id)
.groupBy(m => (m.organization, m.name))
.map { case ((org, name), modules) =>
val versionParsers: Seq[Parser[Option[String]]] =
modules.map { id =>
token(Space ~> id.version).?
}
(Space ~> token(org) ~ token(Space ~> name) ~ oneOf(versionParsers)).map {
case ((org, name), version) => ArtifactPattern(org, name, version)
}
}
.reduceOption(_ | _)
.getOrElse {
// If the dependencyTreeModuleGraphStore couldn't be loaded because no dependency tree command was run before, we should still provide a parser for the command.
((Space ~> token(StringBasic, "<organization>")) ~ (Space ~> token(
StringBasic,
"<module>"
)) ~ (Space ~> token(StringBasic, "<version?>")).?).map { case ((org, mod), version) =>
ArtifactPattern(org, mod, version)
}
}
createArtifactPatternParser(graph)
}
val shouldForceParser: Parser[Boolean] =
(Space ~> (Parser.literal("-f") | "--force")).?.map(_.isDefined)

View File

@ -10,7 +10,17 @@ package sbt
package plugins
import sbt.internal.util.complete.Parser
import DependencyTreeSettings.{ Arg, ArgsParser, Fmt, FmtParser }
import DependencyTreeSettings.{
Arg,
ArgsParser,
Fmt,
FmtParser,
ArtifactPattern,
createArtifactPatternParser
}
import sbt.internal.graph.ModuleGraph
import sbt.internal.graph.GraphModuleId
import sbt.internal.graph.Module
object DependencyTreeTest extends verify.BasicTestSuite:
test("Parse args") {
@ -31,6 +41,77 @@ object DependencyTreeTest extends verify.BasicTestSuite:
assert(parseFormat("graph") == Fmt.Graph)
}
test("ArtifactPatternParser with normal modules") {
val graph = ModuleGraph(
Seq(
node("org1", "name1", "1.0"),
node("org1", "name1", "2.0")
),
Nil
)
val parser = createArtifactPatternParser(graph)
// Test matching
assert(
Parser.parse(" org1 name1 1.0", parser) == Right(
ArtifactPattern("org1", "name1", Some("1.0"))
)
)
assert(
Parser.parse(" org1 name1 2.0", parser) == Right(
ArtifactPattern("org1", "name1", Some("2.0"))
)
)
assert(Parser.parse(" org1 name1", parser) == Right(ArtifactPattern("org1", "name1", None)))
}
test("ArtifactPatternParser with completely empty graph") {
val graph = ModuleGraph.empty
val parser = createArtifactPatternParser(graph)
// Should fallback to generic parser
assert(
Parser.parse(" org1 name1 1.0", parser) == Right(
ArtifactPattern("org1", "name1", Some("1.0"))
)
)
}
test("ArtifactPatternParser should not throw RuntimeException on empty version") {
val graph = ModuleGraph(
Seq(
node("org1", "name1", "")
),
Nil
)
// This previously threw RuntimeException: String literal cannot be empty
val parser = createArtifactPatternParser(graph)
// Should parse org and name, version is None
assert(Parser.parse(" org1 name1", parser) == Right(ArtifactPattern("org1", "name1", None)))
}
test("ArtifactPatternParser mixed valid and empty versions") {
val graph = ModuleGraph(
Seq(
node("org1", "name1", ""),
node("org1", "name1", "1.0")
),
Nil
)
val parser = createArtifactPatternParser(graph)
// Valid version should be selectable
assert(
Parser.parse(" org1 name1 1.0", parser) == Right(
ArtifactPattern("org1", "name1", Some("1.0"))
)
)
// No version should be selectable
assert(Parser.parse(" org1 name1", parser) == Right(ArtifactPattern("org1", "name1", None)))
}
def parseArgs(args: List[String]): Seq[Arg] =
Parser.parse(" " + args.mkString(" "), ArgsParser) match
case Right(args) => args
@ -40,4 +121,8 @@ object DependencyTreeTest extends verify.BasicTestSuite:
Parser.parse(fmt, FmtParser) match
case Right(x) => x
case Left(err) => sys.error(err)
def node(org: String, name: String, version: String): Module =
Module(GraphModuleId(org, name, version), None, "", None, None, None)
end DependencyTreeTest

View File

@ -27,14 +27,14 @@ default:sbt_8ae1da13_2.12:0.1.0-SNAPSHOT [S]
*/
val expectedGraph =
"""default:default-e95e05_2.12:0.1-SNAPSHOT [S]
"""foo:foo_2.12:0.1.0-SNAPSHOT [S]
| +-ch.qos.logback:logback-classic:1.0.7
| | +-ch.qos.logback:logback-core:1.0.7
| | +-org.slf4j:slf4j-api:1.6.6 (evicted by: 1.7.2)
| | +-org.slf4j:slf4j-api:1.7.2
| |
| +-org.slf4j:slf4j-api:1.7.2
| """.stripMargin
// IO.writeLines(file("/tmp/blib"), sanitize(graph).split("\n"))
// IO.writeLines(file("/tmp/blub"), sanitize(expectedGraph).split("\n"))
require(sanitize(graph) == sanitize(expectedGraph), "Graph for report %s was '\n%s' but should have been '\n%s'" format (report, sanitize(graph), sanitize(expectedGraph)))

View File

@ -1,3 +1,3 @@
# to initialize parser with deps
> Compile/dependencyTreeModuleGraph0
> check
> check

View File

@ -1 +1 @@
addDependencyTreePlugin