From 1079975b35d402a2fc597548d4f22210a6565add Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Thu, 24 Jul 2025 15:45:49 +0200 Subject: [PATCH] [`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 --- .../resources/test/fixtures/ruff/RUF033.py | 59 ++++ .../src/rules/ruff/rules/post_init_default.rs | 28 +- ..._rules__ruff__tests__RUF033_RUF033.py.snap | 278 ++++++++++++++++++ 3 files changed, 353 insertions(+), 12 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF033.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF033.py index 95ab12165b..b6a98dabf1 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF033.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF033.py @@ -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 diff --git a/crates/ruff_linter/src/rules/ruff/rules/post_init_default.rs b/crates/ruff_linter/src/rules/ruff/rules/post_init_default.rs index 390c6bb067..dbce5d423f 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/post_init_default.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/post_init_default.rs @@ -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, - ¶meter.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 { + let parameter = ¶meter_with_default.parameter; if current_scope.has(¶meter.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(¶meter.name); - let default = locator.slice(default); let line_ending = checker.stylist().line_ending().as_str(); if let Some(annotation) = ¶meter diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF033_RUF033.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF033_RUF033.py.snap index 073ab755a9..ee5aa75ddb 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF033_RUF033.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF033_RUF033.py.snap @@ -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 | ...