@@ -19,11 +19,11 @@ use {
1919 ruff_text_size:: { Ranged , TextLen , TextRange , TextSize } ,
2020} ;
2121
22+ use super :: NormalizedString ;
23+ use crate :: preview:: is_no_chaperone_for_escaped_quote_in_triple_quoted_docstring_enabled;
2224use crate :: string:: StringQuotes ;
2325use crate :: { prelude:: * , DocstringCodeLineWidth , FormatModuleError } ;
2426
25- use super :: NormalizedString ;
26-
2727/// Format a docstring by trimming whitespace and adjusting the indentation.
2828///
2929/// Summary of changes we make:
@@ -168,7 +168,7 @@ pub(crate) fn format(normalized: &NormalizedString, f: &mut PyFormatter) -> Form
168168 if docstring[ first. len ( ) ..] . trim ( ) . is_empty ( ) {
169169 // For `"""\n"""` or other whitespace between the quotes, black keeps a single whitespace,
170170 // but `""""""` doesn't get one inserted.
171- if needs_chaperone_space ( normalized. flags ( ) , trim_end)
171+ if needs_chaperone_space ( normalized. flags ( ) , trim_end, f . context ( ) )
172172 || ( trim_end. is_empty ( ) && !docstring. is_empty ( ) )
173173 {
174174 space ( ) . fmt ( f) ?;
@@ -208,7 +208,7 @@ pub(crate) fn format(normalized: &NormalizedString, f: &mut PyFormatter) -> Form
208208 let trim_end = docstring
209209 . as_ref ( )
210210 . trim_end_matches ( |c : char | c. is_whitespace ( ) && c != '\n' ) ;
211- if needs_chaperone_space ( normalized. flags ( ) , trim_end) {
211+ if needs_chaperone_space ( normalized. flags ( ) , trim_end, f . context ( ) ) {
212212 space ( ) . fmt ( f) ?;
213213 }
214214
@@ -1596,17 +1596,45 @@ fn docstring_format_source(
15961596 Ok ( formatted. print ( ) ?)
15971597}
15981598
1599- /// If the last line of the docstring is `content" """` or `content\ """`, we need a chaperone space
1600- /// that avoids `content""""` and `content\"""`. This does only applies to un-escaped backslashes,
1601- /// so `content\\ """` doesn't need a space while `content\\\ """` does.
1602- pub ( super ) fn needs_chaperone_space ( flags : AnyStringFlags , trim_end : & str ) -> bool {
1603- if trim_end. chars ( ) . rev ( ) . take_while ( |c| * c == '\\' ) . count ( ) % 2 == 1 {
1604- true
1599+ /// If the last line of the docstring is `content""""` or `content\"""`, we need a chaperone space
1600+ /// that avoids `content""""` and `content\"""`. This only applies to un-escaped backslashes,
1601+ /// so `content\\"""` doesn't need a space while `content\\\"""` does.
1602+ pub ( super ) fn needs_chaperone_space (
1603+ flags : AnyStringFlags ,
1604+ trim_end : & str ,
1605+ context : & PyFormatContext ,
1606+ ) -> bool {
1607+ if count_consecutive_chars_from_end ( trim_end, '\\' ) % 2 == 1 {
1608+ // Odd backslash count; chaperone avoids escaping closing quotes
1609+ // `"\ "` -> prevent that this becomes `"\"` which escapes the closing quote.
1610+ return true ;
1611+ }
1612+
1613+ if is_no_chaperone_for_escaped_quote_in_triple_quoted_docstring_enabled ( context) {
1614+ if flags. is_triple_quoted ( ) {
1615+ if let Some ( before_quote) = trim_end. strip_suffix ( flags. quote_style ( ) . as_char ( ) ) {
1616+ if count_consecutive_chars_from_end ( before_quote, '\\' ) % 2 == 0 {
1617+ // Even backslash count preceding quote;
1618+ // ```py
1619+ // """a " """
1620+ // """a \\" """
1621+ // ```
1622+ // The chaperon is needed or the triple quoted string "ends" with 4 instead of 3 quotes.
1623+ return true ;
1624+ }
1625+ }
1626+ }
1627+
1628+ false
16051629 } else {
16061630 flags. is_triple_quoted ( ) && trim_end. ends_with ( flags. quote_style ( ) . as_char ( ) )
16071631 }
16081632}
16091633
1634+ fn count_consecutive_chars_from_end ( s : & str , target : char ) -> usize {
1635+ s. chars ( ) . rev ( ) . take_while ( |c| * c == target) . count ( )
1636+ }
1637+
16101638#[ derive( Copy , Clone , Debug ) ]
16111639enum Indentation {
16121640 /// Space only indentation or an empty indentation.
0 commit comments