mirror of
https://github.com/github/spec-kit.git
synced 2026-07-03 12:28:06 +08:00
fix(workflows): preserve commas inside quoted list-literal elements (#3134)
* fix(workflows): preserve commas inside quoted list-literal elements
The simple-expression evaluator parsed a list literal with a naive
`inner.split(",")`, which splits on commas inside quoted strings (and
nested brackets). So `{{ ["a, b", "c"] }}` evaluated to three items
(`["a", "b", "c"]`) instead of two, silently corrupting `fan-out` `items:`
and any list expression that contains a comma inside a quoted element.
Split list-literal elements on top-level commas only, ignoring commas
inside quotes or nested brackets, via a small `_split_top_level_commas`
helper. Plain and empty lists are unchanged.
Add tests covering quoted commas, nested lists, and the existing
plain/empty cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(workflows): cover single-quoted and nested list literals
Address review: extend the list-literal regression test to assert single-quoted elements with commas and nested lists parse correctly, alongside the existing double-quoted cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -146,6 +146,40 @@ def _build_namespace(context: Any) -> dict[str, Any]:
|
||||
return ns
|
||||
|
||||
|
||||
def _split_top_level_commas(text: str) -> list[str]:
|
||||
"""Split *text* on commas that are not inside quotes or nested brackets.
|
||||
|
||||
Used for list-literal elements so a quoted element containing a comma
|
||||
(e.g. ``["a, b", "c"]``) is not split mid-string, and nested lists/calls
|
||||
(e.g. ``[[1, 2], 3]``) are kept intact.
|
||||
"""
|
||||
parts: list[str] = []
|
||||
buf: list[str] = []
|
||||
quote: str | None = None
|
||||
depth = 0
|
||||
for ch in text:
|
||||
if quote is not None:
|
||||
buf.append(ch)
|
||||
if ch == quote:
|
||||
quote = None
|
||||
elif ch in ("'", '"'):
|
||||
quote = ch
|
||||
buf.append(ch)
|
||||
elif ch in "([{":
|
||||
depth += 1
|
||||
buf.append(ch)
|
||||
elif ch in ")]}":
|
||||
depth = max(0, depth - 1)
|
||||
buf.append(ch)
|
||||
elif ch == "," and depth == 0:
|
||||
parts.append("".join(buf))
|
||||
buf = []
|
||||
else:
|
||||
buf.append(ch)
|
||||
parts.append("".join(buf))
|
||||
return parts
|
||||
|
||||
|
||||
def _evaluate_simple_expression(expr: str, namespace: dict[str, Any]) -> Any:
|
||||
"""Evaluate a simple expression against the namespace.
|
||||
|
||||
@@ -291,7 +325,10 @@ def _evaluate_simple_expression(expr: str, namespace: dict[str, Any]) -> Any:
|
||||
inner = expr[1:-1].strip()
|
||||
if not inner:
|
||||
return []
|
||||
items = [_evaluate_simple_expression(i.strip(), namespace) for i in inner.split(",")]
|
||||
items = [
|
||||
_evaluate_simple_expression(i.strip(), namespace)
|
||||
for i in _split_top_level_commas(inner)
|
||||
]
|
||||
return items
|
||||
|
||||
# Variable reference (dot-path)
|
||||
|
||||
@@ -268,6 +268,24 @@ class TestExpressions:
|
||||
ctx = StepContext(inputs={"a": False, "b": True})
|
||||
assert evaluate_expression("{{ inputs.a or inputs.b }}", ctx) is True
|
||||
|
||||
def test_list_literal_preserves_quoted_commas(self):
|
||||
from specify_cli.workflows.expressions import evaluate_expression
|
||||
from specify_cli.workflows.base import StepContext
|
||||
|
||||
ctx = StepContext()
|
||||
# commas inside a double-quoted element must not split it
|
||||
assert evaluate_expression('{{ ["a, b", "c"] }}', ctx) == ["a, b", "c"]
|
||||
assert evaluate_expression('{{ ["x, y, z"] }}', ctx) == ["x, y, z"]
|
||||
# single-quoted elements are handled the same way
|
||||
assert evaluate_expression("{{ ['a, b', 'c'] }}", ctx) == ["a, b", "c"]
|
||||
assert evaluate_expression("{{ ['p, q, r'] }}", ctx) == ["p, q, r"]
|
||||
# plain and empty lists still parse correctly
|
||||
assert evaluate_expression("{{ [1, 2, 3] }}", ctx) == [1, 2, 3]
|
||||
assert evaluate_expression("{{ [] }}", ctx) == []
|
||||
# nested lists (commas inside the inner brackets) stay intact
|
||||
assert evaluate_expression('{{ [["a", "b"], "c"] }}', ctx) == [["a", "b"], "c"]
|
||||
assert evaluate_expression("{{ [[1, 2], [3, 4]] }}", ctx) == [[1, 2], [3, 4]]
|
||||
|
||||
def test_filter_default(self):
|
||||
from specify_cli.workflows.expressions import evaluate_expression
|
||||
from specify_cli.workflows.base import StepContext
|
||||
|
||||
Reference in New Issue
Block a user