Preserve triple quotes and prefixes for strings (#15818)

## Summary

This is a follow-up to #15726, #15778, and #15794 to preserve the triple
quote and prefix flags in plain strings, bytestrings, and f-strings.

I also added a `StringLiteralFlags::without_triple_quotes` method to
avoid passing along triple quotes in rules like SIM905 where it might
not make sense, as discussed
[here](https://github.com/astral-sh/ruff/pull/15726#discussion_r1930532426).

## Test Plan

Existing tests, plus many new cases in the `generator::tests::quote`
test that should cover all combinations of quotes and prefixes, at least
for simple string bodies.

Closes #7799 when combined with #15694, #15726, #15778, and #15794.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
Brent Westbrook 2025-02-04 08:41:06 -05:00 committed by GitHub
parent 9a33924a65
commit b5e5271adf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 318 additions and 141 deletions

View file

@ -1,6 +1,6 @@
use itertools::Itertools;
use ruff_formatter::{format_args, write, FormatContext};
use ruff_python_ast::str::Quote;
use ruff_python_ast::str::{Quote, TripleQuotes};
use ruff_python_ast::str_prefix::{
AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix,
};
@ -230,7 +230,7 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> {
}
};
Some(AnyStringFlags::new(prefix, quote, false))
Some(AnyStringFlags::new(prefix, quote, TripleQuotes::No))
}
if !string.is_implicit_concatenated() {

View file

@ -1,6 +1,6 @@
use memchr::memchr2;
pub(crate) use normalize::{normalize_string, NormalizedString, StringNormalizer};
use ruff_python_ast::str::Quote;
use ruff_python_ast::str::{Quote, TripleQuotes};
use ruff_python_ast::StringLikePart;
use ruff_python_ast::{
self as ast,
@ -95,11 +95,11 @@ impl StringLikeExtensions for ast::StringLike<'_> {
fn contains_line_break_or_comments(
elements: &ast::FStringElements,
context: &PyFormatContext,
is_triple_quoted: bool,
triple_quotes: TripleQuotes,
) -> bool {
elements.iter().any(|element| match element {
ast::FStringElement::Literal(literal) => {
is_triple_quoted
triple_quotes.is_yes()
&& context.source().contains_line_break(literal.range())
}
ast::FStringElement::Expression(expression) => {
@ -119,7 +119,7 @@ impl StringLikeExtensions for ast::StringLike<'_> {
contains_line_break_or_comments(
&spec.elements,
context,
is_triple_quoted,
triple_quotes,
)
})
|| expression.debug_text.as_ref().is_some_and(|debug_text| {
@ -134,7 +134,7 @@ impl StringLikeExtensions for ast::StringLike<'_> {
contains_line_break_or_comments(
&f_string.elements,
context,
f_string.flags.is_triple_quoted(),
f_string.flags.triple_quotes(),
)
}
})

View file

@ -5,8 +5,9 @@ use std::iter::FusedIterator;
use ruff_formatter::FormatContext;
use ruff_python_ast::visitor::source_order::SourceOrderVisitor;
use ruff_python_ast::{
str::Quote, AnyStringFlags, BytesLiteral, FString, FStringElement, FStringElements,
FStringFlags, StringFlags, StringLikePart, StringLiteral,
str::{Quote, TripleQuotes},
AnyStringFlags, BytesLiteral, FString, FStringElement, FStringElements, FStringFlags,
StringFlags, StringLikePart, StringLiteral,
};
use ruff_text_size::{Ranged, TextRange, TextSlice};
@ -273,7 +274,7 @@ impl QuoteMetadata {
pub(crate) fn from_str(text: &str, flags: AnyStringFlags, preferred_quote: Quote) -> Self {
let kind = if flags.is_raw_string() {
QuoteMetadataKind::raw(text, preferred_quote, flags.is_triple_quoted())
QuoteMetadataKind::raw(text, preferred_quote, flags.triple_quotes())
} else if flags.is_triple_quoted() {
QuoteMetadataKind::triple_quoted(text, preferred_quote)
} else {
@ -528,7 +529,7 @@ impl QuoteMetadataKind {
/// Computes if a raw string uses the preferred quote. If it does, then it's not possible
/// to change the quote style because it would require escaping which isn't possible in raw strings.
fn raw(text: &str, preferred: Quote, triple_quoted: bool) -> Self {
fn raw(text: &str, preferred: Quote, triple_quotes: TripleQuotes) -> Self {
let mut chars = text.chars().peekable();
let preferred_quote_char = preferred.as_char();
@ -540,7 +541,7 @@ impl QuoteMetadataKind {
}
// `"` or `'`
Some(c) if c == preferred_quote_char => {
if !triple_quoted {
if triple_quotes.is_no() {
break true;
}
@ -1057,7 +1058,7 @@ mod tests {
use std::borrow::Cow;
use ruff_python_ast::{
str::Quote,
str::{Quote, TripleQuotes},
str_prefix::{AnyStringPrefix, ByteStringPrefix},
AnyStringFlags,
};
@ -1086,7 +1087,7 @@ mod tests {
AnyStringFlags::new(
AnyStringPrefix::Bytes(ByteStringPrefix::Regular),
Quote::Double,
false,
TripleQuotes::No,
),
false,
);