Skip to content

Commit e03327f

Browse files
fix(workflows): stop list-literal-then-index expressions silently corrupting (#4572)
_evaluate_simple_expression's list-literal detection was `expr.startswith("[") and expr.endswith("]")` -- true not only for a genuine literal like `[1, 2, 3]` but also for a list literal immediately followed by an index suffix, e.g. `[1,2,3][1]` (read as "index 1 of [1,2,3]", i.e. 2). Naively stripping the outer brackets from that string produces the garbage `1,2,3][1`, which the comma-splitter then breaks into `["1", "2", "3][1"]`; the last segment resolves to None via the dot-path fallback, so `{{ [1,2,3][1] }}` silently evaluated to `[1, 2, None]` instead of raising or resolving the index -- no error, no warning, just wrong data. This is the same "grabs the wrong span" failure mode the adjacent string-literal check already guards against (verifying the matching quote is the last character, not just present), just never given the same treatment for brackets. Added _is_single_list_literal: a quote/bracket-depth scan (mirroring _split_top_level_commas's existing tracking in this same file) that confirms the opening `[` closes exactly at the final character before treating the expression as one literal. A misclassified expression now falls through to the existing dot-path resolution and evaluates to None -- consistent with how every other unresolvable expression in this module already behaves, not a new failure mode. Claude-Session: https://claude.ai/code/session_01U74yBbvVQCPwB7Ed8Dzeu6 Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 9f745f6 commit e03327f

2 files changed

Lines changed: 59 additions & 1 deletion

File tree

src/specify_cli/workflows/expressions.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,41 @@ def _find_top_level(text: str, token: str) -> int:
416416
return -1
417417

418418

419+
def _is_single_list_literal(expr: str) -> bool:
420+
"""Return ``True`` only when *expr* is exactly one bracketed list
421+
literal -- the opening ``[`` closes at the FINAL character, not partway
422+
through the string.
423+
424+
``expr.startswith("[") and expr.endswith("]")`` alone also matches a
425+
list literal immediately followed by an index suffix, e.g.
426+
``[1,2,3][1]`` (meant as "index 1 of [1,2,3]", i.e. 2). Naively
427+
stripping the outer brackets from that string yields the garbage
428+
``1,2,3][1``, which then silently evaluates to ``[1, 2, None]`` instead
429+
of raising or resolving the index -- the same "grabs the wrong span"
430+
failure mode the string-literal check above guards against, just never
431+
given the same treatment for brackets.
432+
"""
433+
if not (expr.startswith("[") and expr.endswith("]")):
434+
return False
435+
quote: str | None = None
436+
depth = 0
437+
n = len(expr)
438+
for i, ch in enumerate(expr):
439+
if quote is not None:
440+
if ch == quote:
441+
quote = None
442+
continue
443+
if ch in ("'", '"'):
444+
quote = ch
445+
elif ch in "([{":
446+
depth += 1
447+
elif ch in ")]}":
448+
depth -= 1
449+
if depth == 0:
450+
return i == n - 1
451+
return False
452+
453+
419454
def _apply_filter(value: Any, filter_expr: str, namespace: dict[str, Any]) -> Any:
420455
"""Apply a single pipe filter segment to *value*.
421456
@@ -647,7 +682,7 @@ def _evaluate_simple_expression(expr: str, namespace: dict[str, Any]) -> Any:
647682
return None
648683

649684
# List literal (simple)
650-
if expr.startswith("[") and expr.endswith("]"):
685+
if _is_single_list_literal(expr):
651686
inner = expr[1:-1].strip()
652687
if not inner:
653688
return []

tests/test_workflows.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,29 @@ def test_list_literal_ignores_trailing_and_empty_commas(self):
416416
# …but an intentional empty-string element is still preserved.
417417
assert evaluate_expression("{{ ['', 'a'] }}", ctx) == ["", "a"]
418418

419+
def test_list_literal_followed_by_index_is_not_misparsed_as_one_literal(self):
420+
"""A list literal immediately followed by an index suffix, e.g.
421+
``[1,2,3][1]``, both starts with ``[`` and ends with ``]`` -- the
422+
same shape as a genuine single list literal. Naively stripping the
423+
outer brackets from ``[1,2,3][1]`` yields ``1,2,3][1``, which then
424+
silently evaluates to ``[1, 2, None]`` instead of raising or
425+
resolving the index. It must not be misclassified as one literal;
426+
falling through to unresolvable (``None``) is safe, unlike silently
427+
returning a wrong-looking list.
428+
"""
429+
from specify_cli.workflows.expressions import evaluate_expression
430+
from specify_cli.workflows.base import StepContext
431+
432+
ctx = StepContext()
433+
assert evaluate_expression("{{ [1,2,3][1] }}", ctx) is None
434+
assert evaluate_expression("{{ [1,2][0] }}", ctx) is None
435+
# Genuine list literals -- including ones a bracket-depth scan must
436+
# still recognize as ending exactly at the final character -- are
437+
# unaffected.
438+
assert evaluate_expression("{{ [1, 2, 3] }}", ctx) == [1, 2, 3]
439+
assert evaluate_expression("{{ [[1, 2], 3] }}", ctx) == [[1, 2], 3]
440+
assert evaluate_expression("{{ ['a]', 'b'] }}", ctx) == ["a]", "b"]
441+
419442
def test_operator_splitting_is_quote_aware(self):
420443
from specify_cli.workflows.expressions import (
421444
evaluate_condition,

0 commit comments

Comments
 (0)