Don't add chaperone space after escaped quote in triple quote (#17216)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / mkdocs (push) Waiting to run
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions

Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
Max Mynter 2025-04-11 10:21:47 +02:00 committed by GitHub
parent 1a3b73720c
commit 1aad180aae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 307 additions and 11 deletions

View file

@ -13,3 +13,10 @@ pub(crate) const fn is_hug_parens_with_braces_and_square_brackets_enabled(
) -> bool {
context.is_preview()
}
/// Returns `true` if the [`no_chaperone_for_escaped_quote_in_triple_quoted_docstring`](https://github.com/astral-sh/ruff/pull/17216) preview style is enabled.
pub(crate) const fn is_no_chaperone_for_escaped_quote_in_triple_quoted_docstring_enabled(
context: &PyFormatContext,
) -> bool {
context.is_preview()
}

View file

@ -19,11 +19,11 @@ use {
ruff_text_size::{Ranged, TextLen, TextRange, TextSize},
};
use super::NormalizedString;
use crate::preview::is_no_chaperone_for_escaped_quote_in_triple_quoted_docstring_enabled;
use crate::string::StringQuotes;
use crate::{prelude::*, DocstringCodeLineWidth, FormatModuleError};
use super::NormalizedString;
/// Format a docstring by trimming whitespace and adjusting the indentation.
///
/// Summary of changes we make:
@ -168,7 +168,7 @@ pub(crate) fn format(normalized: &NormalizedString, f: &mut PyFormatter) -> Form
if docstring[first.len()..].trim().is_empty() {
// For `"""\n"""` or other whitespace between the quotes, black keeps a single whitespace,
// but `""""""` doesn't get one inserted.
if needs_chaperone_space(normalized.flags(), trim_end)
if needs_chaperone_space(normalized.flags(), trim_end, f.context())
|| (trim_end.is_empty() && !docstring.is_empty())
{
space().fmt(f)?;
@ -208,7 +208,7 @@ pub(crate) fn format(normalized: &NormalizedString, f: &mut PyFormatter) -> Form
let trim_end = docstring
.as_ref()
.trim_end_matches(|c: char| c.is_whitespace() && c != '\n');
if needs_chaperone_space(normalized.flags(), trim_end) {
if needs_chaperone_space(normalized.flags(), trim_end, f.context()) {
space().fmt(f)?;
}
@ -1596,17 +1596,45 @@ fn docstring_format_source(
Ok(formatted.print()?)
}
/// If the last line of the docstring is `content" """` or `content\ """`, we need a chaperone space
/// that avoids `content""""` and `content\"""`. This does only applies to un-escaped backslashes,
/// so `content\\ """` doesn't need a space while `content\\\ """` does.
pub(super) fn needs_chaperone_space(flags: AnyStringFlags, trim_end: &str) -> bool {
if trim_end.chars().rev().take_while(|c| *c == '\\').count() % 2 == 1 {
true
/// If the last line of the docstring is `content""""` or `content\"""`, we need a chaperone space
/// that avoids `content""""` and `content\"""`. This only applies to un-escaped backslashes,
/// so `content\\"""` doesn't need a space while `content\\\"""` does.
pub(super) fn needs_chaperone_space(
flags: AnyStringFlags,
trim_end: &str,
context: &PyFormatContext,
) -> bool {
if count_consecutive_chars_from_end(trim_end, '\\') % 2 == 1 {
// Odd backslash count; chaperone avoids escaping closing quotes
// `"\ "` -> prevent that this becomes `"\"` which escapes the closing quote.
return true;
}
if is_no_chaperone_for_escaped_quote_in_triple_quoted_docstring_enabled(context) {
if flags.is_triple_quoted() {
if let Some(before_quote) = trim_end.strip_suffix(flags.quote_style().as_char()) {
if count_consecutive_chars_from_end(before_quote, '\\') % 2 == 0 {
// Even backslash count preceding quote;
// ```py
// """a " """
// """a \\" """
// ```
// The chaperon is needed or the triple quoted string "ends" with 4 instead of 3 quotes.
return true;
}
}
}
false
} else {
flags.is_triple_quoted() && trim_end.ends_with(flags.quote_style().as_char())
}
}
fn count_consecutive_chars_from_end(s: &str, target: char) -> usize {
s.chars().rev().take_while(|c| *c == target).count()
}
#[derive(Copy, Clone, Debug)]
enum Indentation {
/// Space only indentation or an empty indentation.

View file

@ -372,7 +372,7 @@ impl Format<PyFormatContext<'_>> for FormatLiteralContent {
Cow::Owned(normalized) => text(normalized).fmt(f)?,
}
if self.trim_end && needs_chaperone_space(self.flags, &normalized) {
if self.trim_end && needs_chaperone_space(self.flags, &normalized, f.context()) {
space().fmt(f)?;
}
}