mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:24:57 +00:00
[ruff
] Preserve relative whitespace in multi-line expressions (RUF033
) (#19647)
## Summary Fixes #19581 I decided to add in a `indent_first_line` function into [`textwrap.rs`](https://github.com/astral-sh/ruff/blob/main/crates/ruff_python_trivia/src/textwrap.rs), as it solely focuses on text manipulation utilities. It follows the same design as `indent()`, and there may be situations in the future where it can be reused as well. --------- Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
This commit is contained in:
parent
4b80f5fa4f
commit
89ca493fd9
4 changed files with 186 additions and 1 deletions
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue