Skip to content

Commit 74abd4f

Browse files
robsdedudeAlexWaygood
authored andcommitted
[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: #18950
1 parent 7c22db8 commit 74abd4f

3 files changed

Lines changed: 353 additions & 12 deletions

File tree

crates/ruff_linter/resources/test/fixtures/ruff/RUF033.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,62 @@ class Foo:
6565
bar = "should've used attrs"
6666

6767
def __post_init__(self, bar: str = "ahhh", baz: str = "hmm") -> None: ...
68+
69+
70+
# https://github.com/astral-sh/ruff/issues/18950
71+
@dataclass
72+
class Foo:
73+
def __post_init__(self, bar: int = (x := 1)) -> None:
74+
pass
75+
76+
77+
@dataclass
78+
class Foo:
79+
def __post_init__(
80+
self,
81+
bar: int = (x := 1) # comment
82+
,
83+
baz: int = (y := 2), # comment
84+
foo = (a := 1) # comment
85+
,
86+
faz = (b := 2), # comment
87+
) -> None:
88+
pass
89+
90+
91+
@dataclass
92+
class Foo:
93+
def __post_init__(
94+
self,
95+
bar: int = 1, # comment
96+
baz: int = 2, # comment
97+
) -> None:
98+
pass
99+
100+
101+
@dataclass
102+
class Foo:
103+
def __post_init__(
104+
self,
105+
arg1: int = (1) # comment
106+
,
107+
arg2: int = ((1)) # comment
108+
,
109+
arg2: int = (i for i in range(10)) # comment
110+
,
111+
) -> None:
112+
pass
113+
114+
115+
# makes little sense, but is valid syntax
116+
def fun_with_python_syntax():
117+
@dataclass
118+
class Foo:
119+
def __post_init__(
120+
self,
121+
bar: (int) = (yield from range(5)) # comment
122+
,
123+
) -> None:
124+
...
125+
126+
return Foo

crates/ruff_linter/src/rules/ruff/rules/post_init_default.rs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use anyhow::Context;
22

33
use ruff_macros::{ViolationMetadata, derive_message_formats};
44
use ruff_python_ast as ast;
5+
use ruff_python_ast::parenthesize::parenthesized_range;
56
use ruff_python_semantic::{Scope, ScopeKind};
67
use ruff_python_trivia::{indentation_at_offset, textwrap};
78
use ruff_source_file::LineRanges;
@@ -117,13 +118,7 @@ pub(crate) fn post_init_default(checker: &Checker, function_def: &ast::StmtFunct
117118

118119
if !stopped_fixes {
119120
diagnostic.try_set_fix(|| {
120-
use_initvar(
121-
current_scope,
122-
function_def,
123-
&parameter.parameter,
124-
default,
125-
checker,
126-
)
121+
use_initvar(current_scope, function_def, parameter, default, checker)
127122
});
128123
// Need to stop fixes as soon as there is a parameter we cannot fix.
129124
// 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
138133
fn use_initvar(
139134
current_scope: &Scope,
140135
post_init_def: &ast::StmtFunctionDef,
141-
parameter: &ast::Parameter,
136+
parameter_with_default: &ast::ParameterWithDefault,
142137
default: &ast::Expr,
143138
checker: &Checker,
144139
) -> anyhow::Result<Fix> {
140+
let parameter = &parameter_with_default.parameter;
145141
if current_scope.has(&parameter.name) {
146142
return Err(anyhow::anyhow!(
147143
"Cannot add a `{}: InitVar` field to the class body, as a field by that name already exists",
@@ -157,17 +153,25 @@ fn use_initvar(
157153
checker.semantic(),
158154
)?;
159155

156+
let locator = checker.locator();
157+
158+
let default_loc = parenthesized_range(
159+
default.into(),
160+
parameter_with_default.into(),
161+
checker.comment_ranges(),
162+
checker.source(),
163+
)
164+
.unwrap_or(default.range());
165+
160166
// Delete the default value. For example,
161167
// - def __post_init__(self, foo: int = 0) -> None: ...
162168
// + def __post_init__(self, foo: int) -> None: ...
163-
let default_edit = Edit::deletion(parameter.end(), default.end());
169+
let default_edit = Edit::deletion(parameter.end(), default_loc.end());
164170

165171
// Add `dataclasses.InitVar` field to class body.
166-
let locator = checker.locator();
167-
168172
let content = {
173+
let default = locator.slice(default_loc);
169174
let parameter_name = locator.slice(&parameter.name);
170-
let default = locator.slice(default);
171175
let line_ending = checker.stylist().line_ending().as_str();
172176

173177
if let Some(annotation) = &parameter

0 commit comments

Comments
 (0)