From 9315b8410dfedf58d7db9f01b51dda8eb7d2e4c3 Mon Sep 17 00:00:00 2001 From: Kushal Pisavadia Date: Fri, 27 Feb 2026 10:04:32 +0000 Subject: [PATCH] Fix stream double-consumption in `CommandSpecParser` (#1448) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `choices` stream was consumed eagerly for metavar construction, then captured in a lambda for later validation—which promptly fell over with `IllegalStateException`. Materialise to a `List` straightaway. --- .../pkl/core/runtime/CommandSpecParser.java | 7 +++-- .../pkl/core/runtime/CommandSpecParserTest.kt | 27 +++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/CommandSpecParser.java b/pkl-core/src/main/java/org/pkl/core/runtime/CommandSpecParser.java index 0c677556..b80c934a 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/CommandSpecParser.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/CommandSpecParser.java @@ -32,7 +32,6 @@ import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; -import java.util.stream.Collectors; import org.graalvm.collections.EconomicMap; import org.pkl.core.CommandSpec; import org.pkl.core.CommandSpec.Argument; @@ -771,18 +770,18 @@ public final class CommandSpecParser { return true; } else if (typeNode instanceof TypeNode.UnionOfStringLiteralsTypeNode unionOfStringLiteralsTypeNode) { - var choices = unionOfStringLiteralsTypeNode.getStringLiterals().stream().sorted(); + var choices = unionOfStringLiteralsTypeNode.getStringLiterals().stream().sorted().toList(); if (each == null) each = (rawValue, workingDirUri) -> { if (!unionOfStringLiteralsTypeNode.getStringLiterals().contains(rawValue)) { - throw BadValue.invalidChoice(rawValue, choices.toList()); + throw BadValue.invalidChoice(rawValue, choices); } return rawValue; }; if (all == null) all = this::allChooseLast; if (multiple == null) multiple = false; - if (metavar == null) metavar = "[" + choices.collect(Collectors.joining(", ")) + "]"; + if (metavar == null) metavar = "[" + String.join(", ", choices) + "]"; if (completionCandidates == null) completionCandidates = new Fixed(unionOfStringLiteralsTypeNode.getStringLiterals()); return true; diff --git a/pkl-core/src/test/kotlin/org/pkl/core/runtime/CommandSpecParserTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/runtime/CommandSpecParserTest.kt index bafbd485..05aaa1d1 100644 --- a/pkl-core/src/test/kotlin/org/pkl/core/runtime/CommandSpecParserTest.kt +++ b/pkl-core/src/test/kotlin/org/pkl/core/runtime/CommandSpecParserTest.kt @@ -787,4 +787,31 @@ class CommandSpecParserTest { .contains("Option `foo` with annotation `@CountedFlag` has invalid type `String`.") assertThat(exc.message).contains("Expected type: `Int`") } + + @Test + fun `union typed option validates invalid choice without stream error`() { + val moduleUri = + writePklFile( + "cmd.pkl", + renderOptions + + """ + class Options { + format: "json" | "yaml" | "toml" + } + """ + .trimIndent(), + ) + + val spec = parse(moduleUri) + val flag = spec.options.first() as CommandSpec.Flag + + assertThat(flag.metavar()).isEqualTo("[json, toml, yaml]") + + val apply = + assertThrows { + flag.transformEach().apply("xml", URI("file:///tmp")) + } + assertThat(apply.message).contains("invalid choice") + assertThat(apply.message).contains("xml") + } }