Skip to content

Conversation

@robsdedude
Copy link
Contributor

Summary

The generated fix for RUF033 would cause a syntax error for named expressions as parameter defaults.

from dataclasses import InitVar, dataclass
@dataclass
class Foo:
    def __post_init__(self, bar: int = (x := 1)) -> None:
        pass

would be turned into

from dataclasses import InitVar, dataclass
@dataclass
class Foo:
    x: InitVar[int] = x := 1
    def __post_init__(self, bar: int = (x := 1)) -> None:
        pass

instead of the syntactically correct

# ...
x: InitVar[int] = (x := 1)
# ...

Test Plan

Test reproducer (plus some extra tests) have been added to the test suite.

Related

Fixes: #18950

@github-actions
Copy link
Contributor

github-actions bot commented Jul 3, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

@robsdedude robsdedude force-pushed the fix/18950-ruf033-named-expression-as-default branch from 885ff2c to 234e200 Compare July 3, 2025 07:49
@robsdedude robsdedude marked this pull request as ready for review July 3, 2025 08:26
@ntBre ntBre added bug Something isn't working fixes Related to suggested fixes for violations labels Jul 4, 2025
Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

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

Thanks! I think the results look right, but I have a couple of suggestions for the implementation.

Comment on lines 163 to 164
let initvar_name = ast::Expr::Name(ast::ExprName {
node_index: ast::AtomicNodeIndex::dummy(),
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we need to switch all the way to using the code generator here. Can't we just check if the expression is_expr_named and add parens if so? Or I think we could even check the parenthesized_range and preserve the parens it came with.

It's nice to prefer range replacements because the generator strips out any comments, as well as being much more verbose.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I managed to rewrite it using parenthesized_range. It's passing the same tests. But if feels crazy fragile. Looking at the code I cannot convince myself of its correctness and that I haven't missed any edge case. I don't feel comfortable with my name being attached to that code. Sorry.

Copy link
Contributor

Choose a reason for hiding this comment

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

If you feel that strongly about it, we can keep the old implementation. I don't really see how it's more fragile, and we have similar checks in other rules, but this was only a slight preference on my part, not a blocker. I'd much rather fix the bug and give you credit for your work here. I'll approve and merge this if you want to revert/drop the last commit and reopen the PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Where I think this is fragile: if the Syntax ever changes, string replacements like these bake in too many assumptions. That's how this bug happened in the first place: a new syntax construct (walrus operator) was introduced and the replacement logic wasn't up to the task.

Moreover, parenthesized_range calls parentheses_iterator, which in turn is documented with

/// Returns an iterator over the ranges of the optional parentheses surrounding an expression.

Yeah no... these parens are surely not optional 😬 It's quite unfortunate, I find, that the AST does not always include the parens where they're necessary in the expression's range. But neither does Python's own ast module 🤷

I slept a night over it and I'll re-open the PR. If not, I would've given you permission to take my code and commit it under your name. So regardless of how this ends some fix can be merged.

Copy link
Contributor

Choose a reason for hiding this comment

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

Where I think this is fragile: if the Syntax ever changes, string replacements like these bake in too many assumptions. That's how this bug happened in the first place: a new syntax construct (walrus operator) was introduced and the replacement logic wasn't up to the task.

Ahh, I see where you're coming from. I think that explains the other assertion too. I guess I wasn't really thinking about it in the same way since the walrus operator and Python 3.8 predate Ruff by ~3 years. So it wasn't so much that something new came out and broke us, just that the initial rule implementation didn't account for everything that already existed.

Yeah no... these parens are surely not optional

I can also see what you mean here, but I always read this as referring to a Rust Option, like the return type of parenthesized_range, as in the expression may or may not have parentheses rather than may or may not require parentheses.

Anyway, I'm happy with either implementation. I'll give this another quick review now.

@robsdedude robsdedude closed this Jul 22, 2025
@robsdedude robsdedude reopened this Jul 23, 2025
Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

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

Thanks again! Just one nit about a test case name, but this is otherwise good to go from my side.

Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

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

Thank you! I'll merge after the patch release finishes.

@ntBre ntBre merged commit 1079975 into astral-sh:main Jul 24, 2025
35 checks passed
@robsdedude robsdedude deleted the fix/18950-ruf033-named-expression-as-default branch July 24, 2025 14:44
UnboundVariable pushed a commit to UnboundVariable/ruff that referenced this pull request Jul 24, 2025
…hlight

* 'main' of https://github.com/astral-sh/ruff:
  [ty] Minor: fix incomplete docstring (astral-sh#19534)
  [ty] Move server tests as integration tests (astral-sh#19522)
  [`ruff`] Offer fixes for `RUF039` in more cases (astral-sh#19065)
  [ty] Support `dataclasses.InitVar` (astral-sh#19527)
  [`ruff`] Fix `RUF033` breaking with named default expressions (astral-sh#19115)
  Update pre-commit hook name (astral-sh#19530)
  Bump 0.12.5 (astral-sh#19528)
  [ty] Rename type_api => ty_extensions (astral-sh#19523)
  [ty] Added support for "go to references" in ty playground. (astral-sh#19516)
  [ty] Return a tuple spec from the iterator protocol (astral-sh#19496)
  [ty] Exhaustiveness checking & reachability for `match` statements (astral-sh#19508)
  [ty] Fix narrowing and reachability of class patterns with arguments (astral-sh#19512)
UnboundVariable pushed a commit to UnboundVariable/ruff that referenced this pull request Jul 24, 2025
* main:
  [ty] Added support for "document highlights" language server feature. (astral-sh#19515)
  Add support for specifying minimum dots in detected string imports (astral-sh#19538)
  [ty] Minor: fix incomplete docstring (astral-sh#19534)
  [ty] Move server tests as integration tests (astral-sh#19522)
  [`ruff`] Offer fixes for `RUF039` in more cases (astral-sh#19065)
  [ty] Support `dataclasses.InitVar` (astral-sh#19527)
  [`ruff`] Fix `RUF033` breaking with named default expressions (astral-sh#19115)
  Update pre-commit hook name (astral-sh#19530)
  Bump 0.12.5 (astral-sh#19528)
  [ty] Rename type_api => ty_extensions (astral-sh#19523)
  [ty] Added support for "go to references" in ty playground. (astral-sh#19516)

# Conflicts:
#	crates/ty_server/src/server/api/requests.rs
#	crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap
#	crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap
AlexWaygood pushed a commit that referenced this pull request Jul 25, 2025
## Summary
The generated fix for `RUF033` would cause a syntax error for named
expressions as parameter defaults.
```python
from dataclasses import InitVar, dataclass
@DataClass
class Foo:
    def __post_init__(self, bar: int = (x := 1)) -> None:
        pass
```
would be turned into
```python
from dataclasses import InitVar, dataclass
@DataClass
class Foo:
    x: InitVar[int] = x := 1
    def __post_init__(self, bar: int = (x := 1)) -> None:
        pass
```
instead of the syntactically correct
```python
# ...
x: InitVar[int] = (x := 1)
# ...
```

## Test Plan
Test reproducer (plus some extra tests) have been added to the test
suite.

## Related
Fixes: #18950
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working fixes Related to suggested fixes for violations

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RUF033 fix removes necessary parentheses from around the default expression

2 participants