From 8237d4670c9fd6df4e1dcf827a0d956bb39bd6a1 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Sun, 15 Jun 2025 07:53:06 +0200 Subject: [PATCH] Fix `\r` and `\r\n` handling in t- and f-string debug texts (#18673) --- .gitattributes | 3 +++ .../test/fixtures/ruff/.editorconfig | 6 ++++- .../ruff/f-string-carriage-return-newline.py | 8 ++++++ .../src/other/interpolated_string_element.rs | 21 ++++++++++++--- .../ruff_python_formatter/tests/normalizer.rs | 18 +++++++++++++ ...t@f-string-carriage-return-newline.py.snap | 27 +++++++++++++++++++ 6 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/f-string-carriage-return-newline.py create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@f-string-carriage-return-newline.py.snap diff --git a/.gitattributes b/.gitattributes index a12cb611c7..7bc1804074 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,6 +5,9 @@ crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_1.py text eol=crlf crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_2.py text eol=crlf crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_3.py text eol=crlf +crates/ruff_python_formatter/resources/test/fixtures/ruff/f-string-carriage-return-newline.py text eol=crlf +crates/ruff_python_formatter/tests/snapshots/format@f-string-carriage-return-newline.py.snap text eol=crlf + crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_crlf.py text eol=crlf crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_crlf.py.snap text eol=crlf diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/.editorconfig b/crates/ruff_python_formatter/resources/test/fixtures/ruff/.editorconfig index 762b7f0d53..fd6eec1af8 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/.editorconfig +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/.editorconfig @@ -8,4 +8,8 @@ ij_formatter_enabled = false [docstring_tab_indentation.py] generated_code = true -ij_formatter_enabled = false \ No newline at end of file +ij_formatter_enabled = false + +[f-string-carriage-return-newline.py] +generated_code = true +ij_formatter_enabled = false diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/f-string-carriage-return-newline.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/f-string-carriage-return-newline.py new file mode 100644 index 0000000000..1ddc0eee67 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/f-string-carriage-return-newline.py @@ -0,0 +1,8 @@ +# Regression test for https://github.com/astral-sh/ruff/issues/18667 +f"{ +1= +}" + +t"{ +1= +}" diff --git a/crates/ruff_python_formatter/src/other/interpolated_string_element.rs b/crates/ruff_python_formatter/src/other/interpolated_string_element.rs index e0b53331ea..19e243f86a 100644 --- a/crates/ruff_python_formatter/src/other/interpolated_string_element.rs +++ b/crates/ruff_python_formatter/src/other/interpolated_string_element.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use ruff_formatter::{Buffer, RemoveSoftLinesBuffer, format_args, write}; +use ruff_formatter::{Buffer, FormatOptions as _, RemoveSoftLinesBuffer, format_args, write}; use ruff_python_ast::{ AnyStringFlags, ConversionFlag, Expr, InterpolatedElement, InterpolatedStringElement, InterpolatedStringLiteralElement, StringFlags, @@ -178,9 +178,9 @@ impl Format> for FormatInterpolatedElement<'_> { write!( f, [ - text(&debug_text.leading), + NormalizedDebugText(&debug_text.leading), verbatim_text(&**expression), - text(&debug_text.trailing), + NormalizedDebugText(&debug_text.trailing), ] )?; @@ -316,3 +316,18 @@ fn needs_bracket_spacing(expr: &Expr, context: &PyFormatContext) -> bool { Expr::Dict(_) | Expr::DictComp(_) | Expr::Set(_) | Expr::SetComp(_) ) } + +struct NormalizedDebugText<'a>(&'a str); + +impl Format> for NormalizedDebugText<'_> { + fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { + let normalized = normalize_newlines(self.0, ['\r']); + + f.write_element(FormatElement::Text { + text_width: TextWidth::from_text(&normalized, f.options().indent_width()), + text: normalized.into_owned().into_boxed_str(), + }); + + Ok(()) + } +} diff --git a/crates/ruff_python_formatter/tests/normalizer.rs b/crates/ruff_python_formatter/tests/normalizer.rs index b89ec5130b..e5237f8c5f 100644 --- a/crates/ruff_python_formatter/tests/normalizer.rs +++ b/crates/ruff_python_formatter/tests/normalizer.rs @@ -196,6 +196,24 @@ impl Transformer for Normalizer { transformer::walk_expr(self, expr); } + fn visit_interpolated_string_element( + &self, + interpolated_string_element: &mut InterpolatedStringElement, + ) { + let InterpolatedStringElement::Interpolation(interpolation) = interpolated_string_element + else { + return; + }; + + let Some(debug) = &mut interpolation.debug_text else { + return; + }; + + // Changing the newlines to the configured newline is okay because Python normalizes all newlines to `\n` + debug.leading = debug.leading.replace("\r\n", "\n").replace('\r', "\n"); + debug.trailing = debug.trailing.replace("\r\n", "\n").replace('\r', "\n"); + } + fn visit_string_literal(&self, string_literal: &mut ast::StringLiteral) { static STRIP_DOC_TESTS: LazyLock = LazyLock::new(|| { Regex::new( diff --git a/crates/ruff_python_formatter/tests/snapshots/format@f-string-carriage-return-newline.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@f-string-carriage-return-newline.py.snap new file mode 100644 index 0000000000..c24f246a5e --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@f-string-carriage-return-newline.py.snap @@ -0,0 +1,27 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/f-string-carriage-return-newline.py +--- +## Input +```python +# Regression test for https://github.com/astral-sh/ruff/issues/18667 +f"{ +1= +}" + +t"{ +1= +}" +``` + +## Output +```python +# Regression test for https://github.com/astral-sh/ruff/issues/18667 +f"{ +1= +}" + +t"{ +1= +}" +```