Skip to content

[perflint] Avoid invalid comprehension suggestions (PERF401, PERF403) - #29366

Merged
ntBre merged 4 commits into
astral-sh:mainfrom
fixfon:fix/perf401-ternary-iterable
Oct 14, 2026
Merged

ntBre merged 4 commits into
astral-sh:mainfrom
fixfon:fix/perf401-ternary-iterable

Conversation

@fixfon

@fixfon fixfon commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

Fixes #29365.

When the loop iterable was a ternary (or a lambda), the PERF401/PERF403 fix pasted it into the comprehension as-is, which isn't valid Python, so Ruff rolled the fix back. Now the iterable gets parenthesized in those cases. If the iterable contains a walrus, I just skip the fix and keep the diagnostic, since that can't go in a comprehension iterable even with parens.

Test plan: added the examples from the issue plus ternary, lambda and walrus cases to the PERF401/PERF403 fixtures and updated the snapshots. cargo test -p ruff_linter perflint passes, and the repro from the issue now fixes cleanly.

I used Claude to help with this change and reviewed and tested it myself.

…`PERF401`, `PERF403`)

The fixes copied the loop iterable into the comprehension as-is, adding
parentheses only for an implicit tuple. A conditional expression or lambda
iterable produced invalid code, so the fix was reverted with a syntax error.
Iterables containing an assignment expression can't be moved into a
comprehension at all, so no fix is offered for them.

Fixes astral-sh#29365
@astral-sh-bot
astral-sh-bot Bot requested a review from ntBre October 14, 2026 07:23
@astral-sh-bot

astral-sh-bot Bot commented Oct 14, 2026

Copy link
Copy Markdown

ruff-ecosystem results

Linter (stable)

βœ… ecosystem check detected no linter changes.

Linter (preview)

βœ… ecosystem check detected no linter changes.

Comment on lines +410 to +417
let for_iter_str = match &*for_stmt.iter {
Expr::Tuple(ast::ExprTuple {
parenthesized: false,
..
})
| Expr::Lambda(_)
| Expr::If(_) => format!("({})", locator.slice(for_stmt.iter.range())),
_ => locator.slice(for_stmt.iter.range()).to_string(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use this helper here and in the other rule?

pub(super) fn parenthesize_loop_iter_if_necessary<'a>(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched both rules to the refurb helper and made it pub(crate). Existing snapshots didn't change. It did start duplicating a comment inside the iterable's parentheses, so I now ignore the parenthesized range when collecting comments. That's in the same commit.

Comment on lines +392 to +403
// if the loop target was an implicit tuple, add parentheses around it
// A comprehension's iterable can't be an unparenthesized tuple, lambda, or conditional
// expression, so add parentheses around it. Parentheses in the original source (e.g.,
// `for x in (a if b else c)`) are outside the expression's range, so they're added back.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming we use the helper, we'll need to adjust the comments again too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed them, the helper's doc comment already covers this.

Comment on lines +376 to +378
return Err(anyhow!(
"Cannot move an assignment expression into a comprehension iterable"
));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem like the right place for this check. Returning an Err means this will be logged by diagnostic.try_set_fix, which I think makes more sense for unexpected errors than control flow. We should either return a Result<Option<Fix>> or perform this check before calling this function. The latter seems fine here and a bit easier.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the walrus check before the fix, so there's no Err anymore. For PERF403 I left it as is, since convert_to_dict_comprehension already returns Option<Fix>. Can move it up too if you prefer it consistent.

@ntBre ntBre added the bug An issue describing something that isn't working, or a PR that fixes a bug label Oct 14, 2026
fixfon and others added 3 commits October 14, 2026 19:08
Use `parenthesize_loop_iter_if_necessary` from `refurb` to parenthesize the
loop iterable in `PERF401` and `PERF403` instead of matching on the
expression kind in each rule, and drop the comments that described that
logic. The helper is made `pub(crate)` so that it can be shared.

Because the helper keeps any parentheses around the iterable, and the
comments inside them, exclude the parenthesized range when collecting the
comments to move above the replaced statement. Otherwise such a comment was
kept in place and also duplicated above the statement.

For `PERF401`, check for an assignment expression in the iterable before
building the fix rather than returning an `Err` from the fix function, since
`try_set_fix` logs errors and this is an expected case, not a failure.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

@ntBre ntBre left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I pushed a couple of commits returning the range from the helper to avoid having to compute it again in each rule and also returning without a diagnostic rather than without a fix when a named expression is used in the comprehension iterable. If we can't offer an autofix, it seems like the user won't really be able to make a manual fix either, at least without more significantly changing the structure of their code.

@ntBre ntBre changed the title [perflint] Parenthesize conditional and lambda iterables in fixes (PERF401, PERF403) [perflint] Avoid invalid comprehension suggestions (PERF401, PERF403) Oct 14, 2026
@ntBre
ntBre merged commit 9352615 into astral-sh:main Oct 14, 2026
83 of 84 checks passed
moylop260 pushed a commit to Vauxoo/ruff-odoo that referenced this pull request Oct 15, 2026
Brings the fork from upstream main at 9520164 (after 0.16.10) to
9352615, the merge of astral-sh#29366, which stops PERF401 and
PERF403 from suggesting invalid comprehensions: the 0.17.0 release plus
the 64 commits after it, 83 upstream commits in all.

Conflicts were limited to the version lines: Cargo.toml and pyproject.toml
take upstream's versions, keeping the `=` pin on ruff_linter and the
ruff-odoo package metadata, and uv.lock is upstream's relocked with uv
0.13.1. The fork's release counter is kept, so 0.16.10.35 becomes
0.17.0.35 in .bumpversion.cfg, the READMEs and the wrapper crate.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug An issue describing something that isn't working, or a PR that fixes a bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PERF401/PERF403 fixes introduce a syntax error when the loop iterable is a ternary expression

2 participants