Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/ruff/RUF033.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,19 @@ def __post_init__(
...

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
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
115 changes: 115 additions & 0 deletions crates/ruff_python_trivia/src/textwrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down
Loading