Skip to content

Commit 37f5d61

Browse files
RUF072: skip formfeeds on dedent (astral-sh#24308)
Co-authored-by: Micha Reiser <micha@reiser.io>
1 parent b03dc78 commit 37f5d61

3 files changed

Lines changed: 227 additions & 21 deletions

File tree

‎crates/ruff_linter/resources/test/fixtures/ruff/RUF072.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,10 @@
170170
foo()
171171
finally: # comment
172172
pass
173+
174+
# Bare try finally with line starting with a formfeed
175+
try:
176+
1
177+
2
178+
finally:
179+
pass

‎crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF072_RUF072.py.snap‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,5 +324,28 @@ RUF072 Empty `finally` clause
324324
171 | / finally: # comment
325325
172 | | pass
326326
| |________^
327+
173 |
328+
174 | # Bare try finally with line starting with a formfeed
327329
|
328330
help: Remove the `finally` clause
331+
332+
RUF072 [*] Empty `finally` clause
333+
--> RUF072.py:178:1
334+
|
335+
176 | 1
336+
177 | 2
337+
178 | / finally:
338+
179 | | pass
339+
| |________^
340+
|
341+
help: Remove the `finally` clause
342+
172 | pass
343+
173 |
344+
174 | # Bare try finally with line starting with a formfeed
345+
- try:
346+
- 1
347+
- 2
348+
- finally:
349+
- pass
350+
175 + 1
351+
176 + 2

‎crates/ruff_python_trivia/src/textwrap.rs‎

Lines changed: 197 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -197,25 +197,37 @@ pub fn dedent(text: &str) -> Cow<'_, str> {
197197
///
198198
/// Lines that consist solely of whitespace are trimmed to a blank line.
199199
///
200+
/// Lines that start with formfeeds have the indentation after the formfeeds
201+
/// removed and the formfeeds reinstated
202+
///
200203
/// # Panics
201204
/// If the first line is indented by less than the provided indent.
202205
pub fn dedent_to(text: &str, indent: &str) -> Option<String> {
203206
// Look at the indentation of the first non-empty line, to determine the "baseline" indentation.
204-
let mut first_comment = None;
207+
let mut first_comment_indent = None;
205208
let existing_indent_len = text
206209
.universal_newlines()
207210
.find_map(|line| {
208-
let trimmed = line.trim_whitespace_start();
211+
// Following Python's lexer, treat form feed character's at the start of a line
212+
// the same as a line break (reset the indentation)
213+
let trimmed_start_of_line_formfeed = line.trim_start_matches('\x0C');
214+
let trimmed = trimmed_start_of_line_formfeed.trim_whitespace_start();
215+
216+
// A whitespace only line
209217
if trimmed.is_empty() {
210-
None
211-
} else if trimmed.starts_with('#') && first_comment.is_none() {
212-
first_comment = Some(line.len() - trimmed.len());
218+
return None;
219+
}
220+
221+
let indent_len = trimmed_start_of_line_formfeed.len() - trimmed.len();
222+
223+
if trimmed.starts_with('#') && first_comment_indent.is_none() {
224+
first_comment_indent = Some(indent_len);
213225
None
214226
} else {
215-
Some(line.len() - trimmed.len())
227+
Some(indent_len)
216228
}
217229
})
218-
.unwrap_or(first_comment.unwrap_or_default());
230+
.unwrap_or(first_comment_indent.unwrap_or_default());
219231

220232
if existing_indent_len < indent.len() {
221233
return None;
@@ -225,23 +237,38 @@ pub fn dedent_to(text: &str, indent: &str) -> Option<String> {
225237
let dedent_len = existing_indent_len - indent.len();
226238

227239
let mut result = String::with_capacity(text.len() + indent.len());
240+
228241
for line in text.universal_newlines() {
229-
let trimmed = line.trim_whitespace_start();
230-
if trimmed.is_empty() {
231-
if let Some(line_ending) = line.line_ending() {
232-
result.push_str(&line_ending);
233-
}
242+
let line_content = line.trim_start_matches('\x0C');
243+
let formfeed_count = line.len() - line_content.len();
244+
245+
let line_ending = if let Some(line_ending) = line.line_ending() {
246+
line_ending.as_str()
234247
} else {
235-
// Determine the current indentation level.
236-
let current_indent_len = line.len() - trimmed.len();
237-
if current_indent_len < existing_indent_len {
238-
// If the current indentation level is less than the baseline, keep it as is.
239-
result.push_str(line.as_full_str());
240-
} else {
241-
// Otherwise, reduce the indentation level.
242-
result.push_str(&line.as_full_str()[dedent_len..]);
243-
}
248+
""
249+
};
250+
251+
let line_without_indent = line.trim_whitespace_start();
252+
253+
if line_without_indent.is_empty() {
254+
result.push_str(line_ending);
255+
continue;
244256
}
257+
258+
// Determine the current indentation level.
259+
let current_indent_len = line_content.len() - line_without_indent.len();
260+
261+
if current_indent_len < existing_indent_len {
262+
// If the current indentation level is less than the baseline, keep it as is.
263+
result.push_str(line.as_full_str());
264+
continue;
265+
}
266+
let dedented_content = &line_content[dedent_len..];
267+
268+
let formfeeds = &line[..formfeed_count];
269+
result.push_str(formfeeds);
270+
result.push_str(dedented_content);
271+
result.push_str(line_ending);
245272
}
246273
Some(result)
247274
}
@@ -576,5 +603,154 @@ mod tests {
576603
" baz"
577604
].join("\n");
578605
assert_eq!(dedent_to(&x, " "), Some(y));
606+
607+
let x = [
608+
"\x0C 1",
609+
" 2"
610+
].join("\n");
611+
let y = [
612+
"\x0C1",
613+
"2"
614+
].join("\n");
615+
assert_eq!(dedent_to(&x, ""), Some(y));
616+
}
617+
618+
#[test]
619+
#[rustfmt::skip]
620+
fn dedent_to_returns_none_if_indent_too_large() {
621+
let x = [
622+
" foo",
623+
" bar"
624+
].join("\n");
625+
assert_eq!(dedent_to(&x, " "), None);
626+
}
627+
628+
#[test]
629+
#[rustfmt::skip]
630+
fn dedent_to_only_whitespace_lines() {
631+
let x = [
632+
" ",
633+
"\t",
634+
" "
635+
].join("\n");
636+
let y = "\n\n".to_string();
637+
assert_eq!(dedent_to(&x, ""), Some(y));
638+
}
639+
640+
#[test]
641+
#[rustfmt::skip]
642+
fn dedent_to_preserves_crlf_for_lines_starting_with_form_feed() {
643+
let x = [
644+
"\x0C 1\r\n",
645+
" 2\r\n",
646+
].join("");
647+
let y = [
648+
"\x0C1\r\n",
649+
"2\r\n",
650+
].join("");
651+
assert_eq!(dedent_to(&x, ""), Some(y));
652+
}
653+
654+
#[test]
655+
#[rustfmt::skip]
656+
fn dedent_to_preserves_multiple_leading_form_feeds_on_first_line() {
657+
let x = [
658+
"\x0C\x0C 1",
659+
" 2",
660+
].join("\n");
661+
let y = [
662+
"\x0C\x0C1",
663+
"2",
664+
].join("\n");
665+
assert_eq!(dedent_to(&x, ""), Some(y));
666+
}
667+
668+
#[test]
669+
#[rustfmt::skip]
670+
fn dedent_to_preserves_multiple_leading_form_feeds_on_second_line() {
671+
let x = [
672+
" 1",
673+
"\x0C\x0C 2",
674+
].join("\n");
675+
let y = [
676+
"1",
677+
"\x0C\x0C2",
678+
].join("\n");
679+
assert_eq!(dedent_to(&x, ""), Some(y));
680+
}
681+
682+
#[test]
683+
#[rustfmt::skip]
684+
fn dedent_to_handles_when_multiple_leading_form_feeds_greater_than_dedent_len() {
685+
let x = [
686+
"\x0C\x0C\x0C\x0C 1",
687+
" 2",
688+
].join("\n");
689+
let y = [
690+
"\x0C\x0C\x0C\x0C1",
691+
"2",
692+
].join("\n");
693+
assert_eq!(dedent_to(&x, ""), Some(y));
694+
}
695+
696+
#[test]
697+
#[rustfmt::skip]
698+
fn dedent_to_ignores_leading_form_feeds_when_checking_indentation() {
699+
let x = [
700+
" 1",
701+
"\x0C\x0C 2",
702+
].join("\n");
703+
let y = [
704+
"1",
705+
"\x0C\x0C 2",
706+
].join("\n");
707+
assert_eq!(dedent_to(&x, ""), Some(y));
708+
}
709+
710+
#[test]
711+
#[rustfmt::skip]
712+
fn dedent_to_is_idempotent() {
713+
let x = [
714+
" foo",
715+
" bar",
716+
" ",
717+
" baz"
718+
].join("\n");
719+
let y = [
720+
" foo",
721+
" bar",
722+
"",
723+
" baz"
724+
].join("\n");
725+
let first_result = dedent_to(&x, " ").unwrap();
726+
assert_eq!(dedent_to(&first_result, " "), Some(y));
727+
}
728+
729+
#[test]
730+
#[rustfmt::skip]
731+
fn dedent_to_preserves_less_indented_later_line() {
732+
let x = [
733+
" foo\n",
734+
" bar\n",
735+
].join("");
736+
let y = [
737+
"foo\n",
738+
" bar\n",
739+
].join("");
740+
assert_eq!(dedent_to(&x, ""), Some(y));
741+
}
742+
743+
#[test]
744+
#[rustfmt::skip]
745+
fn dedent_to_preserves_less_indented_later_line_with_crlf() {
746+
let x = [
747+
" foo\r\n",
748+
" bar\r\n",
749+
].join("");
750+
let y = [
751+
"foo\r\n",
752+
" bar\r\n",
753+
].join("");
754+
assert_eq!(dedent_to(&x, ""), Some(y));
579755
}
580756
}

0 commit comments

Comments
 (0)