Skip to content

fix(workflows): make expression operator/literal parsing quote-aware#3197

Open
jawwad-ali wants to merge 1 commit into
github:mainfrom
jawwad-ali:fix/workflow-expression-quote-aware-operators
Open

fix(workflows): make expression operator/literal parsing quote-aware#3197
jawwad-ali wants to merge 1 commit into
github:mainfrom
jawwad-ali:fix/workflow-expression-quote-aware-operators

Conversation

@jawwad-ali

Copy link
Copy Markdown
Contributor

Description

The workflow expression evaluator (_evaluate_simple_expression) split on operator keywords with naive str.find/split, so a keyword inside a quoted operand was mistaken for an operator. Two concrete bugs:

  1. Keyword inside a quoted string{{ inputs.mode == 'read and write' }} split on the inner and and evaluated as (inputs.mode == 'read) and (write') -> wrong result.
  2. Greedy literal short-circuit'a' == 'b' matched startswith("'") and endswith("'") and was stripped to the garbage truthy string a' == 'b. So {{ 'done' == 'failed' }} evaluated truthy and gated the wrong branch in if/switch/while/fan-out steps.

The docstring already lists comparisons, and/or/not, in/not in, and string literals as supported, so silent mis-parsing of common human-readable strings ("approve or reject", "read and write", "in progress") contradicts the contract.

Fix

  • Add a quote/bracket-aware _find_top_level(text, token) helper — a direct sibling of the existing _split_top_level_commas (added in fix(workflows): preserve commas inside quoted list-literal elements #3134) — and use it for the or/and/comparison/in/not in splits, so operators inside quoted operands are skipped.
  • Tighten the literal short-circuit to fire only when the opening quote's matching close is the final character (otherwise fall through to operator parsing).

Pipe-filter handling is untouched (it runs first and already worked for single filters).

Testing

  • uvx ruff check clean.
  • New TestExpressions::test_operator_splitting_is_quote_aware — fails before / passes after; covers keyword-in-literal (both operand sides), membership, literal-vs-literal equality, single-literal preservation, plus regression guards (plain comparisons, real and/or, | default('a and b')).
  • Full tests/test_workflows.py: 305 passed, 1 skipped. (5 pre-existing symlink tests fail only on Windows due to the SeCreateSymbolicLink privilege — identical on clean main with this change stashed; they pass on CI Linux.)
  • Verified each case against the real module on main (b7e67f5).

AI Disclosure

  • I did use AI assistance (describe below)

Found and fixed with Claude Code (Claude Opus 4.8) under my direction. AI located the quote-naive splitting + greedy literal short-circuit, mirrored the existing _split_top_level_commas pattern, and drafted the regression tests; I reproduced every mis-parse against the real evaluator, confirmed the regression guards (filter args, real boolean operators, list literals) still pass, and reviewed the diff before submitting.

_evaluate_simple_expression split on operator keywords using naive str.find/split, so a keyword INSIDE a quoted operand was treated as an operator: `inputs.mode == 'read and write'` split on the inner ' and ' and evaluated as `(inputs.mode == 'read) and (write')`. The literal short-circuit was also too greedy -- `'a' == 'b'` matched startswith("'")/endswith("'") and was stripped to the garbage truthy string `a' == 'b`, so `'done' == 'failed'` evaluated truthy and gated the wrong branch.

Add a quote/bracket-aware _find_top_level helper (mirroring the existing _split_top_level_commas) and use it for the and/or/comparison/in/not-in splits; tighten the literal short-circuit to fire only when the opening quote's match is the final char. The docstring already lists comparisons + and/or/not + in/not-in + string literals as supported, so this restores the documented contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jawwad-ali jawwad-ali requested a review from mnriem as a code owner June 27, 2026 13:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant