fix(workflows): reject a multi-argument filter call in expressions - #3893
fix(workflows): reject a multi-argument filter call in expressions#3893jawwad-ali wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds strict validation for unsupported multi-argument workflow filter calls.
Changes:
- Detects top-level commas in filter arguments.
- Adds regression tests for multi-argument calls and quoted commas.
Show a summary per file
| File | Description |
|---|---|
src/specify_cli/workflows/expressions.py |
Adds filter argument validation. |
tests/test_workflows.py |
Tests invalid multiple arguments and valid quoted commas. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Balanced
mnriem
left a comment
There was a problem hiding this comment.
Please address Copilot feedback
49ed86a to
e36a61c
Compare
|
This is a clean fix — green review, reproduced, disclosed, and CI passes. The only blocker is that the branch now conflicts with |
`_apply_filter` parses a call with `re.fullmatch(r"(\w+)\((.+)\)")` and
hands the ENTIRE captured argument text to `_evaluate_simple_expression`
as one expression. Every filter in this subset takes exactly one argument,
so a two-argument call is not a valid expression and evaluates to None:
{{ inputs.missing | default(1) }} -> 1
{{ inputs.missing | default(1, 2) }} -> None <-- silently wrong
{{ inputs.name | join(",", "extra") }} -> ValueError: join: expected a
string separator, got NoneType
So `default` silently returns None instead of its default, and `join`
raises a message blaming the separator rather than the extra argument.
Fall through to the existing "unsupported form" error, which names the
filter and lists the accepted forms:
filter 'default' used in an unsupported form (got '| default(1, 2)'): ...
The check is quote-aware, because a single argument may legitimately
contain a comma — `join(", ")` and `default("a, b")` must keep working, so
a plain split would reject valid expressions.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…r check
Review catch: my hand-rolled `_has_top_level_comma` was quote-aware but NOT
bracket-aware, so it treated the comma inside a list literal as an argument
separator. The evaluator supports list literals, so this rejected
expressions that work on main today:
main: {{ inputs.missing | default([1, 2]) }} -> [1, 2]
with my PR: ValueError: filter 'default' used in an unsupported form
That is a breaking change, not a fix.
Drop the helper and use `_find_top_level`, the same scanner the operator
splitting already uses — it skips commas inside quotes AND inside nested
brackets. Verified:
default([1, 2]) -> [1, 2] (restored)
default([1,2]) -> [1, 2] (restored)
default([]) -> [] (restored)
join(", ") / default("a, b") -> unchanged
default(1, 2) -> rejected (the actual bug)
join(",", "extra") -> rejected
default([1,2], 3) -> rejected (real 2nd arg after a literal)
Dict literals resolve to None both before and after, matching main.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…omment
Addresses review feedback: `_evaluate_simple_expression` implements a list
literal branch only. A mapping such as `{"a": 1}` has no branch there and
falls through to dot-path resolution:
list literal [1, 2] -> [1, 2]
dict literal {"a": 1} -> None
So the comment's claim that the evaluator supports dict literals was wrong.
Comment-only; no behaviour change. `_find_top_level` is brace-aware as well as
bracket-aware (`_find_top_level('{"a": 1, "b": 2}', ',')` returns -1), so the
scanner treats such a comma as nested either way -- the example was simply
describing syntax the evaluator does not implement, which is exactly the kind
of thing a future change might have relied on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
c59cf00 to
5536c88
Compare
|
Rebased onto current The conflict came from my own merged #3894, which added
All five now coexist; the diff against Re-verified after the rebase:
@mnriem — the Copilot thread on this PR was addressed back in c59cf00 and Copilot's follow-up review on Sep 1 filed nothing further, so I believe the |
Problem
_apply_filterparses a filter call with:The entire captured argument text goes to
_evaluate_simple_expressionas a single expression. Every filter in this subset takes exactly one argument, so"1, 2"is not a valid expression — it evaluates toNone.Reproduction on current
main(81bf741)Two distinct failures:
defaultsilently returnsNone— the opposite of the filter's entire purpose, with no error. A workflow using it gets an empty interpolation and carries on.joinraises a misleading error. It blames the separator ("expected a string separator, got NoneType") when the separator was fine and the real problem is the extra argument.Fix
Fall through to the error this function already raises for a registered filter used wrongly:
That message names the filter and lists the accepted forms, which is exactly the diagnostic the author needs.
The check has to be quote-aware
A single argument may legitimately contain a comma, so a plain
split(",")would reject valid expressions. All of these must keep working, and are pinned by a new test:Hence
_has_top_level_comma, which tracks quote state and only reports a comma outside a quoted span.Breaking risk: the only expressions whose behaviour changes are multi-argument calls, which today either return
Noneor raise a misleading error — neither is a form anyone can be relying on. Every single-argument form, the no-argument| default, and chained filters are unchanged; verified above and by the existingTestExpressionssuite.Verification
test_multi_argument_filter_call_fails_loudlyfails on unpatchedsrcand passes with the fix.tests/test_workflows.py: 21 failed → 20 failed, 838 → 839 passed.mainbaseline captured on81bf741(all Windows symlink-privilege).uvx ruff@0.15.0 check src tests→ cleanTests sit beside the existing filter-strictness tests (
test_registered_filter_unsupported_form_raises,test_filter_call_with_trailing_tokens_fails_loudly), which established this exact "fail loudly rather than silently mis-evaluate" contract.Written with assistance from Claude Code. Bug found, reproduced, and verified by me on current
main.