[`ruff`] Fix `RUF033` breaking with named default expressions (#19115)

## 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: https://github.com/astral-sh/ruff/issues/18950
This commit is contained in:
Robsdedude 2025-07-24 15:45:49 +02:00 committed by GitHub
parent 39eb0f6c6c
commit 1079975b35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 353 additions and 12 deletions

View File

@ -65,3 +65,62 @@ class Foo:
bar = "should've used attrs"
def __post_init__(self, bar: str = "ahhh", baz: str = "hmm") -> None: ...
# https://github.com/astral-sh/ruff/issues/18950
@dataclass
class Foo:
def __post_init__(self, bar: int = (x := 1)) -> None:
pass
@dataclass
class Foo:
def __post_init__(
self,
bar: int = (x := 1) # comment
,
baz: int = (y := 2), # comment
foo = (a := 1) # comment
,
faz = (b := 2), # comment
) -> None:
pass
@dataclass
class Foo:
def __post_init__(
self,
bar: int = 1, # comment
baz: int = 2, # comment
) -> None:
pass
@dataclass
class Foo:
def __post_init__(
self,
arg1: int = (1) # comment
,
arg2: int = ((1)) # comment
,
arg2: int = (i for i in range(10)) # comment
,
) -> None:
pass
# makes little sense, but is valid syntax
def fun_with_python_syntax():
@dataclass
class Foo:
def __post_init__(
self,
bar: (int) = (yield from range(5)) # comment
,
) -> None:
...
return Foo

View File

@ -2,6 +2,7 @@ use anyhow::Context;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_semantic::{Scope, ScopeKind};
use ruff_python_trivia::{indentation_at_offset, textwrap};
use ruff_source_file::LineRanges;
@ -117,13 +118,7 @@ pub(crate) fn post_init_default(checker: &Checker, function_def: &ast::StmtFunct
if !stopped_fixes {
diagnostic.try_set_fix(|| {
use_initvar(
current_scope,
function_def,
&parameter.parameter,
default,
checker,
)
use_initvar(current_scope, function_def, parameter, default, checker)
});
// Need to stop fixes as soon as there is a parameter we cannot fix.
// Otherwise, we risk a syntax error (a parameter without a default
@ -138,10 +133,11 @@ pub(crate) fn post_init_default(checker: &Checker, function_def: &ast::StmtFunct
fn use_initvar(
current_scope: &Scope,
post_init_def: &ast::StmtFunctionDef,
parameter: &ast::Parameter,
parameter_with_default: &ast::ParameterWithDefault,
default: &ast::Expr,
checker: &Checker,
) -> anyhow::Result<Fix> {
let parameter = &parameter_with_default.parameter;
if current_scope.has(&parameter.name) {
return Err(anyhow::anyhow!(
"Cannot add a `{}: InitVar` field to the class body, as a field by that name already exists",
@ -157,17 +153,25 @@ fn use_initvar(
checker.semantic(),
)?;
let locator = checker.locator();
let default_loc = parenthesized_range(
default.into(),
parameter_with_default.into(),
checker.comment_ranges(),
checker.source(),
)
.unwrap_or(default.range());
// Delete the default value. For example,
// - def __post_init__(self, foo: int = 0) -> None: ...
// + def __post_init__(self, foo: int) -> None: ...
let default_edit = Edit::deletion(parameter.end(), default.end());
let default_edit = Edit::deletion(parameter.end(), default_loc.end());
// Add `dataclasses.InitVar` field to class body.
let locator = checker.locator();
let content = {
let default = locator.slice(default_loc);
let parameter_name = locator.slice(&parameter.name);
let default = locator.slice(default);
let line_ending = checker.stylist().line_ending().as_str();
if let Some(annotation) = &parameter

View File

@ -156,3 +156,281 @@ RUF033.py:67:59: RUF033 `__post_init__` method with argument defaults
| ^^^^^ RUF033
|
= help: Use `dataclasses.InitVar` instead
RUF033.py:73:41: RUF033 [*] `__post_init__` method with argument defaults
|
71 | @dataclass
72 | class Foo:
73 | def __post_init__(self, bar: int = (x := 1)) -> None:
| ^^^^^^ RUF033
74 | pass
|
= help: Use `dataclasses.InitVar` instead
Unsafe fix
70 70 | # https://github.com/astral-sh/ruff/issues/18950
71 71 | @dataclass
72 72 | class Foo:
73 |- def __post_init__(self, bar: int = (x := 1)) -> None:
73 |+ bar: InitVar[int] = (x := 1)
74 |+ def __post_init__(self, bar: int) -> None:
74 75 | pass
75 76 |
76 77 |
RUF033.py:81:21: RUF033 [*] `__post_init__` method with argument defaults
|
79 | def __post_init__(
80 | self,
81 | bar: int = (x := 1) # comment
| ^^^^^^ RUF033
82 | ,
83 | baz: int = (y := 2), # comment
|
= help: Use `dataclasses.InitVar` instead
Unsafe fix
76 76 |
77 77 | @dataclass
78 78 | class Foo:
79 |+ bar: InitVar[int] = (x := 1)
79 80 | def __post_init__(
80 81 | self,
81 |- bar: int = (x := 1) # comment
82 |+ bar: int # comment
82 83 | ,
83 84 | baz: int = (y := 2), # comment
84 85 | foo = (a := 1) # comment
RUF033.py:83:21: RUF033 [*] `__post_init__` method with argument defaults
|
81 | bar: int = (x := 1) # comment
82 | ,
83 | baz: int = (y := 2), # comment
| ^^^^^^ RUF033
84 | foo = (a := 1) # comment
85 | ,
|
= help: Use `dataclasses.InitVar` instead
Unsafe fix
76 76 |
77 77 | @dataclass
78 78 | class Foo:
79 |+ baz: InitVar[int] = (y := 2)
79 80 | def __post_init__(
80 81 | self,
81 82 | bar: int = (x := 1) # comment
82 83 | ,
83 |- baz: int = (y := 2), # comment
84 |+ baz: int, # comment
84 85 | foo = (a := 1) # comment
85 86 | ,
86 87 | faz = (b := 2), # comment
RUF033.py:84:16: RUF033 [*] `__post_init__` method with argument defaults
|
82 | ,
83 | baz: int = (y := 2), # comment
84 | foo = (a := 1) # comment
| ^^^^^^ RUF033
85 | ,
86 | faz = (b := 2), # comment
|
= help: Use `dataclasses.InitVar` instead
Unsafe fix
76 76 |
77 77 | @dataclass
78 78 | class Foo:
79 |+ foo: InitVar = (a := 1)
79 80 | def __post_init__(
80 81 | self,
81 82 | bar: int = (x := 1) # comment
82 83 | ,
83 84 | baz: int = (y := 2), # comment
84 |- foo = (a := 1) # comment
85 |+ foo # comment
85 86 | ,
86 87 | faz = (b := 2), # comment
87 88 | ) -> None:
RUF033.py:86:16: RUF033 [*] `__post_init__` method with argument defaults
|
84 | foo = (a := 1) # comment
85 | ,
86 | faz = (b := 2), # comment
| ^^^^^^ RUF033
87 | ) -> None:
88 | pass
|
= help: Use `dataclasses.InitVar` instead
Unsafe fix
76 76 |
77 77 | @dataclass
78 78 | class Foo:
79 |+ faz: InitVar = (b := 2)
79 80 | def __post_init__(
80 81 | self,
81 82 | bar: int = (x := 1) # comment
--------------------------------------------------------------------------------
83 84 | baz: int = (y := 2), # comment
84 85 | foo = (a := 1) # comment
85 86 | ,
86 |- faz = (b := 2), # comment
87 |+ faz, # comment
87 88 | ) -> None:
88 89 | pass
89 90 |
RUF033.py:95:20: RUF033 [*] `__post_init__` method with argument defaults
|
93 | def __post_init__(
94 | self,
95 | bar: int = 1, # comment
| ^ RUF033
96 | baz: int = 2, # comment
97 | ) -> None:
|
= help: Use `dataclasses.InitVar` instead
Unsafe fix
90 90 |
91 91 | @dataclass
92 92 | class Foo:
93 |+ bar: InitVar[int] = 1
93 94 | def __post_init__(
94 95 | self,
95 |- bar: int = 1, # comment
96 |+ bar: int, # comment
96 97 | baz: int = 2, # comment
97 98 | ) -> None:
98 99 | pass
RUF033.py:96:20: RUF033 [*] `__post_init__` method with argument defaults
|
94 | self,
95 | bar: int = 1, # comment
96 | baz: int = 2, # comment
| ^ RUF033
97 | ) -> None:
98 | pass
|
= help: Use `dataclasses.InitVar` instead
Unsafe fix
90 90 |
91 91 | @dataclass
92 92 | class Foo:
93 |+ baz: InitVar[int] = 2
93 94 | def __post_init__(
94 95 | self,
95 96 | bar: int = 1, # comment
96 |- baz: int = 2, # comment
97 |+ baz: int, # comment
97 98 | ) -> None:
98 99 | pass
99 100 |
RUF033.py:105:22: RUF033 [*] `__post_init__` method with argument defaults
|
103 | def __post_init__(
104 | self,
105 | arg1: int = (1) # comment
| ^ RUF033
106 | ,
107 | arg2: int = ((1)) # comment
|
= help: Use `dataclasses.InitVar` instead
Unsafe fix
100 100 |
101 101 | @dataclass
102 102 | class Foo:
103 |+ arg1: InitVar[int] = (1)
103 104 | def __post_init__(
104 105 | self,
105 |- arg1: int = (1) # comment
106 |+ arg1: int # comment
106 107 | ,
107 108 | arg2: int = ((1)) # comment
108 109 | ,
RUF033.py:107:23: RUF033 [*] `__post_init__` method with argument defaults
|
105 | arg1: int = (1) # comment
106 | ,
107 | arg2: int = ((1)) # comment
| ^ RUF033
108 | ,
109 | arg2: int = (i for i in range(10)) # comment
|
= help: Use `dataclasses.InitVar` instead
Unsafe fix
100 100 |
101 101 | @dataclass
102 102 | class Foo:
103 |+ arg2: InitVar[int] = ((1))
103 104 | def __post_init__(
104 105 | self,
105 106 | arg1: int = (1) # comment
106 107 | ,
107 |- arg2: int = ((1)) # comment
108 |+ arg2: int # comment
108 109 | ,
109 110 | arg2: int = (i for i in range(10)) # comment
110 111 | ,
RUF033.py:109:21: RUF033 [*] `__post_init__` method with argument defaults
|
107 | arg2: int = ((1)) # comment
108 | ,
109 | arg2: int = (i for i in range(10)) # comment
| ^^^^^^^^^^^^^^^^^^^^^^ RUF033
110 | ,
111 | ) -> None:
|
= help: Use `dataclasses.InitVar` instead
Unsafe fix
100 100 |
101 101 | @dataclass
102 102 | class Foo:
103 |+ arg2: InitVar[int] = (i for i in range(10))
103 104 | def __post_init__(
104 105 | self,
105 106 | arg1: int = (1) # comment
106 107 | ,
107 108 | arg2: int = ((1)) # comment
108 109 | ,
109 |- arg2: int = (i for i in range(10)) # comment
110 |+ arg2: int # comment
110 111 | ,
111 112 | ) -> None:
112 113 | pass
RUF033.py:121:27: RUF033 [*] `__post_init__` method with argument defaults
|
119 | def __post_init__(
120 | self,
121 | bar: (int) = (yield from range(5)) # comment
| ^^^^^^^^^^^^^^^^^^^ RUF033
122 | ,
123 | ) -> None:
|
= help: Use `dataclasses.InitVar` instead
Unsafe fix
116 116 | def fun_with_python_syntax():
117 117 | @dataclass
118 118 | class Foo:
119 |+ bar: InitVar[int] = (yield from range(5))
119 120 | def __post_init__(
120 121 | self,
121 |- bar: (int) = (yield from range(5)) # comment
122 |+ bar: (int) # comment
122 123 | ,
123 124 | ) -> None:
124 125 | ...