diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a6dd074a..92652f6ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: diff --git a/main/src/main/scala/sbt/plugins/DependencyTreeSettings.scala b/main/src/main/scala/sbt/plugins/DependencyTreeSettings.scala index 0f14c7dcb..ed47af3af 100644 --- a/main/src/main/scala/sbt/plugins/DependencyTreeSettings.scala +++ b/main/src/main/scala/sbt/plugins/DependencyTreeSettings.scala @@ -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, "")) ~ (Space ~> token( + StringBasic, + "" + )) ~ (Space ~> token(StringBasic, "")).?).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, "")) ~ (Space ~> token( - StringBasic, - "" - )) ~ (Space ~> token(StringBasic, "")).?).map { case ((org, mod), version) => - ArtifactPattern(org, mod, version) - } - } + createArtifactPatternParser(graph) } + val shouldForceParser: Parser[Boolean] = (Space ~> (Parser.literal("-f") | "--force")).?.map(_.isDefined) diff --git a/main/src/test/scala/sbt/plugins/DependencyTreeTest.scala b/main/src/test/scala/sbt/plugins/DependencyTreeTest.scala index 89942aecf..cb34bb14f 100644 --- a/main/src/test/scala/sbt/plugins/DependencyTreeTest.scala +++ b/main/src/test/scala/sbt/plugins/DependencyTreeTest.scala @@ -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 diff --git a/sbt-app/src/sbt-test/dependency-graph/ignoreScalaLibrary/build.sbt b/sbt-app/src/sbt-test/dependency-graph/ignoreScalaLibrary/build.sbt index 0f5889a1c..0a9848dc9 100644 --- a/sbt-app/src/sbt-test/dependency-graph/ignoreScalaLibrary/build.sbt +++ b/sbt-app/src/sbt-test/dependency-graph/ignoreScalaLibrary/build.sbt @@ -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))) diff --git a/sbt-app/src/sbt-test/dependency-graph/whatDependsOn-without-previous-initialization/project/plugins.sbt b/sbt-app/src/sbt-test/dependency-graph/whatDependsOn-without-previous-initialization/project/plugins.sbt index 93c66d2a9..8b1378917 100644 --- a/sbt-app/src/sbt-test/dependency-graph/whatDependsOn-without-previous-initialization/project/plugins.sbt +++ b/sbt-app/src/sbt-test/dependency-graph/whatDependsOn-without-previous-initialization/project/plugins.sbt @@ -1 +1 @@ -addDependencyTreePlugin + diff --git a/sbt-app/src/sbt-test/dependency-graph/whatDependsOn/pending b/sbt-app/src/sbt-test/dependency-graph/whatDependsOn/pending index 111738e33..9bf597e6c 100644 --- a/sbt-app/src/sbt-test/dependency-graph/whatDependsOn/pending +++ b/sbt-app/src/sbt-test/dependency-graph/whatDependsOn/pending @@ -1,3 +1,3 @@ # to initialize parser with deps > Compile/dependencyTreeModuleGraph0 -> check \ No newline at end of file +> check diff --git a/sbt-app/src/sbt-test/dependency-graph/whatDependsOn/project/plugins.sbt b/sbt-app/src/sbt-test/dependency-graph/whatDependsOn/project/plugins.sbt index 93c66d2a9..8b1378917 100644 --- a/sbt-app/src/sbt-test/dependency-graph/whatDependsOn/project/plugins.sbt +++ b/sbt-app/src/sbt-test/dependency-graph/whatDependsOn/project/plugins.sbt @@ -1 +1 @@ -addDependencyTreePlugin +