diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF033.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF033.py index b6a98dabf1..f7a72a9dae 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF033.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF033.py @@ -124,3 +124,19 @@ def fun_with_python_syntax(): ... return Foo + + +@dataclass +class C: + def __post_init__(self, x: tuple[int, ...] = ( + 1, + 2, + )) -> None: + self.x = x + + +@dataclass +class D: + def __post_init__(self, x: int = """ + """) -> None: + self.x = x 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 dbce5d423f..b2a2da56a9 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 @@ -186,7 +186,7 @@ fn use_initvar( let indentation = indentation_at_offset(post_init_def.start(), checker.source()) .context("Failed to calculate leading indentation of `__post_init__` method")?; - let content = textwrap::indent(&content, indentation); + let content = textwrap::indent_first_line(&content, indentation); let initvar_edit = Edit::insertion( content.into_owned(), 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 f73734afe8..e3dc5e551e 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 @@ -455,3 +455,57 @@ help: Use `dataclasses.InitVar` instead 122 123 | , 123 124 | ) -> None: 124 125 | ... + +RUF033 [*] `__post_init__` method with argument defaults + --> RUF033.py:131:50 + | +129 | @dataclass +130 | class C: +131 | def __post_init__(self, x: tuple[int, ...] = ( + | __________________________________________________^ +132 | | 1, +133 | | 2, +134 | | )) -> None: + | |_____^ +135 | self.x = x + | +help: Use `dataclasses.InitVar` instead + +ℹ Unsafe fix +128 128 | +129 129 | @dataclass +130 130 | class C: +131 |- def __post_init__(self, x: tuple[int, ...] = ( + 131 |+ x: InitVar[tuple[int, ...]] = ( +132 132 | 1, +133 133 | 2, +134 |- )) -> None: + 134 |+ ) + 135 |+ def __post_init__(self, x: tuple[int, ...]) -> None: +135 136 | self.x = x +136 137 | +137 138 | + +RUF033 [*] `__post_init__` method with argument defaults + --> RUF033.py:140:38 + | +138 | @dataclass +139 | class D: +140 | def __post_init__(self, x: int = """ + | ______________________________________^ +141 | | """) -> None: + | |_______^ +142 | self.x = x + | +help: Use `dataclasses.InitVar` instead + +ℹ Unsafe fix +137 137 | +138 138 | @dataclass +139 139 | class D: +140 |- def __post_init__(self, x: int = """ +141 |- """) -> None: + 140 |+ x: InitVar[int] = """ + 141 |+ """ + 142 |+ def __post_init__(self, x: int) -> None: +142 143 | self.x = x diff --git a/crates/ruff_python_trivia/src/textwrap.rs b/crates/ruff_python_trivia/src/textwrap.rs index ce5f46671a..50ce0cd08c 100644 --- a/crates/ruff_python_trivia/src/textwrap.rs +++ b/crates/ruff_python_trivia/src/textwrap.rs @@ -71,6 +71,66 @@ pub fn indent<'a>(text: &'a str, prefix: &str) -> Cow<'a, str> { Cow::Owned(result) } +/// Indent only the first line by the given prefix. +/// +/// This function is useful when you want to indent the first line of a multi-line +/// expression while preserving the relative indentation of subsequent lines. +/// +/// # Examples +/// +/// ``` +/// # use ruff_python_trivia::textwrap::indent_first_line; +/// +/// assert_eq!(indent_first_line("First line.\nSecond line.\n", " "), +/// " First line.\nSecond line.\n"); +/// ``` +/// +/// When indenting, trailing whitespace is stripped from the prefix. +/// This means that empty lines remain empty afterwards: +/// +/// ``` +/// # use ruff_python_trivia::textwrap::indent_first_line; +/// +/// assert_eq!(indent_first_line("\n\n\nSecond line.\n", " "), +/// "\n\n\nSecond line.\n"); +/// ``` +/// +/// Leading and trailing whitespace coming from the text itself is +/// kept unchanged: +/// +/// ``` +/// # use ruff_python_trivia::textwrap::indent_first_line; +/// +/// assert_eq!(indent_first_line(" \t Foo ", "->"), "-> \t Foo "); +/// ``` +pub fn indent_first_line<'a>(text: &'a str, prefix: &str) -> Cow<'a, str> { + if prefix.is_empty() { + return Cow::Borrowed(text); + } + + let mut lines = text.universal_newlines(); + let Some(first_line) = lines.next() else { + return Cow::Borrowed(text); + }; + + let mut result = String::with_capacity(text.len() + prefix.len()); + + // Indent only the first line + if first_line.trim_whitespace().is_empty() { + result.push_str(prefix.trim_whitespace_end()); + } else { + result.push_str(prefix); + } + result.push_str(first_line.as_full_str()); + + // Add remaining lines without indentation + for line in lines { + result.push_str(line.as_full_str()); + } + + Cow::Owned(result) +} + /// Removes common leading whitespace from each line. /// /// This function will look at each non-empty line and determine the @@ -409,6 +469,61 @@ mod tests { assert_eq!(dedent(text), text); } + #[test] + fn indent_first_line_empty() { + assert_eq!(indent_first_line("\n", " "), "\n"); + } + + #[test] + #[rustfmt::skip] + fn indent_first_line_nonempty() { + let text = [ + " foo\n", + "bar\n", + " baz\n", + ].join(""); + let expected = [ + "// foo\n", + "bar\n", + " baz\n", + ].join(""); + assert_eq!(indent_first_line(&text, "// "), expected); + } + + #[test] + #[rustfmt::skip] + fn indent_first_line_empty_line() { + let text = [ + " foo", + "bar", + "", + " baz", + ].join("\n"); + let expected = [ + "// foo", + "bar", + "", + " baz", + ].join("\n"); + assert_eq!(indent_first_line(&text, "// "), expected); + } + + #[test] + #[rustfmt::skip] + fn indent_first_line_mixed_newlines() { + let text = [ + " foo\r\n", + "bar\n", + " baz\r", + ].join(""); + let expected = [ + "// foo\r\n", + "bar\n", + " baz\r", + ].join(""); + assert_eq!(indent_first_line(&text, "// "), expected); + } + #[test] #[rustfmt::skip] fn adjust_indent() {