diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_newlines.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_newlines.py new file mode 100644 index 0000000000000..b350a3f9ec71f --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_newlines.py @@ -0,0 +1,55 @@ +# Tests that Ruff correctly preserves newlines before the closing quote + +def test(): + """a, b +c""" + + +def test2(): + """a, b + c +""" + +def test3(): + """a, b + +""" + + +def test4(): + """ + a, b + +""" + + +def test5(): + """ + a, b +a""" + +def test6(): + """ + a, b + + c +""" + + + +def test7(): + """ + a, b + + + + +""" + +def test7(): + """ + a, b + + + + """ diff --git a/crates/ruff_python_formatter/src/string/docstring.rs b/crates/ruff_python_formatter/src/string/docstring.rs index 1086889605f16..ba73519a604c0 100644 --- a/crates/ruff_python_formatter/src/string/docstring.rs +++ b/crates/ruff_python_formatter/src/string/docstring.rs @@ -6,6 +6,7 @@ use std::{borrow::Cow, collections::VecDeque}; use ruff_formatter::printer::SourceMapGeneration; use ruff_python_parser::ParseError; + use {once_cell::sync::Lazy, regex::Regex}; use { ruff_formatter::{write, FormatOptions, IndentStyle, LineWidth, Printed}, @@ -114,7 +115,10 @@ pub(crate) fn format(normalized: &NormalizedString, f: &mut PyFormatter) -> Form // is_borrowed is unstable :/ let already_normalized = matches!(docstring, Cow::Borrowed(_)); - let mut lines = docstring.lines().peekable(); + // Use `split` instead of `lines` to preserve the closing quotes on their own line + // if they have no indentation (in which case the last line is `\n` which + // `lines` omit for the last element). + let mut lines = docstring.split('\n').peekable(); // Start the string write!(f, [normalized.prefix, normalized.quotes])?; @@ -259,7 +263,7 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> { /// iterator given contains all lines except for the first. fn add_iter( &mut self, - mut lines: std::iter::Peekable>, + mut lines: std::iter::Peekable>, ) -> FormatResult<()> { while let Some(line) = lines.next() { let line = InputDocstringLine { diff --git a/crates/ruff_python_formatter/tests/snapshots/format@docstring_newlines.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@docstring_newlines.py.snap new file mode 100644 index 0000000000000..ee05a1d3338b8 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@docstring_newlines.py.snap @@ -0,0 +1,125 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_newlines.py +--- +## Input +```python +# Tests that Ruff correctly preserves newlines before the closing quote + +def test(): + """a, b +c""" + + +def test2(): + """a, b + c +""" + +def test3(): + """a, b + +""" + + +def test4(): + """ + a, b + +""" + + +def test5(): + """ + a, b +a""" + +def test6(): + """ + a, b + + c +""" + + + +def test7(): + """ + a, b + + + + +""" + +def test7(): + """ + a, b + + + + """ +``` + +## Output +```python +# Tests that Ruff correctly preserves newlines before the closing quote + + +def test(): + """a, b + c""" + + +def test2(): + """a, b + c + """ + + +def test3(): + """a, b""" + + +def test4(): + """ + a, b + + """ + + +def test5(): + """ + a, b + a""" + + +def test6(): + """ + a, b + + c + """ + + +def test7(): + """ + a, b + + + + + """ + + +def test7(): + """ + a, b + + + + """ +``` + + +