From 20f4a9c3b2907d08c84be9b72c326c51c470f021 Mon Sep 17 00:00:00 2001 From: bitloi <89318445+bitloi@users.noreply.github.com> Date: Sun, 22 Feb 2026 23:43:38 -0500 Subject: [PATCH] [2.x] fix: Fixes java++ tab completion (#8778) Fixes #4310 Per review, the OOM comes from the version parser - token(StringBasic) on the right side of || has no examples constraint. Move .examples(knownVersions*) from the inner number parser to wrap the entire || expression, so both alternatives are bounded. --- main/src/main/scala/sbt/internal/CrossJava.scala | 12 +++++++++--- main/src/test/scala/sbt/internal/CrossJavaTest.scala | 11 +++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/main/src/main/scala/sbt/internal/CrossJava.scala b/main/src/main/scala/sbt/internal/CrossJava.scala index 845773b7e..813dd4c33 100644 --- a/main/src/main/scala/sbt/internal/CrossJava.scala +++ b/main/src/main/scala/sbt/internal/CrossJava.scala @@ -132,6 +132,9 @@ private[sbt] object CrossJava { private case class SwitchTarget(version: Option[JavaVersion], home: Option[File], force: Boolean) private case class SwitchJavaHome(target: SwitchTarget, verbose: Boolean, command: Option[String]) + private[sbt] val JavaSwitchCommandCompletions = + Vector("-v", "compile", "test", "run", "clean", "console", "package") + private def switchParser(state: State): Parser[SwitchJavaHome] = { import DefaultParsers.* def versionAndCommand(spacePresent: Boolean) = { @@ -141,9 +144,9 @@ private[sbt] object CrossJava { val knownVersions = javaHomes.keysIterator.map(_.numberStr).toVector val version: Parser[SwitchTarget] = (token( - (StringBasic <~ "@").? ~ ((NatBasic) ~ ("." ~> NatBasic).*) - .examples(knownVersions*) ~ "!".? + (StringBasic <~ "@").? ~ ((NatBasic) ~ ("." ~> NatBasic).*) ~ "!".? ) || token(StringBasic)) + .examples(knownVersions*) .map { case Left(((vendor, (v1, vs)), bang)) => val force = bang.isDefined @@ -156,7 +159,10 @@ private[sbt] object CrossJava { if (spacePresent) version else version & spacedFirst(JavaSwitchCommand) val verbose = Parser.opt(token(Space ~> "-v")) - val optionalCommand = Parser.opt(token(Space ~> matched(state.combinedParser))) + val optionalCommand = + Parser.opt( + token(Space ~> matched(state.combinedParser).examples(JavaSwitchCommandCompletions*)) + ) (spacedVersion ~ verbose ~ optionalCommand).map { case v ~ verbose ~ command => SwitchJavaHome(v, verbose.isDefined, command) } diff --git a/main/src/test/scala/sbt/internal/CrossJavaTest.scala b/main/src/test/scala/sbt/internal/CrossJavaTest.scala index bfd8c8059..8b10be51b 100644 --- a/main/src/test/scala/sbt/internal/CrossJavaTest.scala +++ b/main/src/test/scala/sbt/internal/CrossJavaTest.scala @@ -242,4 +242,15 @@ class CrossJavaTest extends AnyFunSuite with Diagrams { assert(version == "temurin@11.0.15") } } + + test("java++ tab completion list is bounded (#4310)"): + val completions = CrossJava.JavaSwitchCommandCompletions + assert(completions.nonEmpty, "completion list must not be empty") + assert( + completions.size <= 20, + "completion list must stay small to avoid JLine 'Display all N possibilities?'" + ) + assert(completions.contains("-v"), "completion list should suggest -v") + assert(completions.contains("compile"), "completion list should suggest compile") + assert(completions.contains("test"), "completion list should suggest test") }