fix(workflows): evaluate parenthesised expressions - #4417
Open
NgoQuocViet2001 wants to merge 1 commit into
Open
Conversation
The operator scans in _evaluate_simple_expression skip over bracketed text, so an operator inside a nested operand is never split on. Nothing then unwrapped a group spanning the whole expression: `(a or b) and c` split at the top-level `and`, evaluated `(a or b)` as a dot path, found no such key, and got None. The `or` was never evaluated and the expression read false. So adding parentheses to make precedence explicit — the usual reason to add them — silently inverted the result: `inputs.a or inputs.b and inputs.c` was true while `(inputs.a or inputs.b) and inputs.c` was false. A step gated on such a condition is skipped with nothing reported; the malformed-condition validators do not flag it, because the syntax is valid. Unwrap a group that spans the whole expression, quote-aware and only when the opening paren closes at the very end, so `(a) and (b)` and a literal paren inside a string are untouched.
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Parenthesis handling must also be mirrored in _unresolvable_term() with remediation coverage.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds correct evaluation of fully parenthesized workflow expressions.
Changes:
- Unwraps complete parenthesized groups before evaluation.
- Adds grouping, nesting, filtering, and quote-awareness tests.
File summaries
| File | Review |
|---|---|
tests/test_workflows.py |
Adds regression coverage for grouped expressions and edge cases. |
src/specify_cli/workflows/expressions.py |
Implements parenthesis unwrapping. The remediation parser remains inconsistent with evaluator behavior for grouped bare conditions (moderate; 2 votes). |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+536
to
+537
| if _is_wrapped_in_parens(expr): | ||
| return _evaluate_simple_expression(expr[1:-1], namespace) |
mnriem
requested changes
Sep 3, 2026
mnriem
left a comment
Collaborator
There was a problem hiding this comment.
Please address Copilot feedback
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Adding parentheses to a workflow expression silently inverts its result:
_find_top_leveldeliberately skips bracketed text so an operator inside a nested operand is not split on. Nothing then unwraps a group that spans the whole expression, so:(a or b) and csplits at the top-leveland(a or b)reaches the dot-path fallback_resolve_dot_pathlooks up a key literally named(a or b), finds nothing, returnsNoneThe
oris never evaluated and the expression reads false. Parentheses are added precisely to make precedence explicit, so this fires on the expressions an author was being careful with.Nothing catches it either: the syntax is valid, so
condition_has_malformed_expression_blockandcondition_is_never_evaluatedboth pass it. Anif:step gated on such a condition takes the wrong branch, and awhilestep never starts, with no diagnostic.Fix
Unwrap a group that spans the whole expression, before the operator scans run. The check is quote-aware and requires the opening paren to close at the very last character, so it does not touch:
(a) and (b)— the first group closes early, so the top-levelandstill splits(inputs.n) | default(9)— same reason; the filter still applies'a(b'and('(')— a paren inside a string literal is not a groupScope
One helper and one early return in
src/specify_cli/workflows/expressions.py. Unparenthesised expressions take exactly the path they did before.Test plan
pytest tests/test_workflows.py -k "parenthesised or indexing or Expressions"→ 54 passed.pytest tests/test_workflows.py→ 942 passed. The 20 failures are theTestWorkflowCliAlignmentsymlink cases, which fail identically on an unmodified checkout here (Windows, no symlink privilege).expressions.pyfails the new test withassert False is True.