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

@ -13,10 +13,10 @@ use itertools::Itertools;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::name::Name;
use crate::{
int,
str::Quote,
name::Name,
str::{Quote, TripleQuotes},
str_prefix::{AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix},
ExceptHandler, Expr, FStringElement, LiteralExpressionRef, Pattern, Stmt, TypeParam,
};
@ -981,25 +981,24 @@ pub trait StringFlags: Copy {
/// Does the string use single or double quotes in its opener and closer?
fn quote_style(self) -> Quote;
/// Is the string triple-quoted, i.e.,
/// does it begin and end with three consecutive quote characters?
fn is_triple_quoted(self) -> bool;
fn triple_quotes(self) -> TripleQuotes;
fn prefix(self) -> AnyStringPrefix;
/// Is the string triple-quoted, i.e.,
/// does it begin and end with three consecutive quote characters?
fn is_triple_quoted(self) -> bool {
self.triple_quotes().is_yes()
}
/// A `str` representation of the quotes used to start and close.
/// This does not include any prefixes the string has in its opener.
fn quote_str(self) -> &'static str {
if self.is_triple_quoted() {
match self.quote_style() {
Quote::Single => "'''",
Quote::Double => r#"""""#,
}
} else {
match self.quote_style() {
Quote::Single => "'",
Quote::Double => "\"",
}
match (self.triple_quotes(), self.quote_style()) {
(TripleQuotes::Yes, Quote::Single) => "'''",
(TripleQuotes::Yes, Quote::Double) => r#"""""#,
(TripleQuotes::No, Quote::Single) => "'",
(TripleQuotes::No, Quote::Double) => "\"",
}
}
@ -1028,10 +1027,30 @@ pub trait StringFlags: Copy {
self.quote_len()
}
fn format_string_contents(self, contents: &str) -> String {
let prefix = self.prefix();
let quote_str = self.quote_str();
format!("{prefix}{quote_str}{contents}{quote_str}")
fn display_contents(self, contents: &str) -> DisplayFlags {
DisplayFlags {
prefix: self.prefix(),
quote_str: self.quote_str(),
contents,
}
}
}
pub struct DisplayFlags<'a> {
prefix: AnyStringPrefix,
quote_str: &'a str,
contents: &'a str,
}
impl std::fmt::Display for DisplayFlags<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{prefix}{quote}{contents}{quote}",
prefix = self.prefix,
quote = self.quote_str,
contents = self.contents
)
}
}
@ -1097,8 +1116,9 @@ impl FStringFlags {
}
#[must_use]
pub fn with_triple_quotes(mut self) -> Self {
self.0 |= FStringFlagsInner::TRIPLE_QUOTED;
pub fn with_triple_quotes(mut self, triple_quotes: TripleQuotes) -> Self {
self.0
.set(FStringFlagsInner::TRIPLE_QUOTED, triple_quotes.is_yes());
self
}
@ -1132,8 +1152,12 @@ impl StringFlags for FStringFlags {
/// Return `true` if the f-string is triple-quoted, i.e.,
/// it begins and ends with three consecutive quote characters.
/// For example: `f"""{bar}"""`
fn is_triple_quoted(self) -> bool {
self.0.contains(FStringFlagsInner::TRIPLE_QUOTED)
fn triple_quotes(self) -> TripleQuotes {
if self.0.contains(FStringFlagsInner::TRIPLE_QUOTED) {
TripleQuotes::Yes
} else {
TripleQuotes::No
}
}
/// Return the quoting style (single or double quotes)
@ -1477,8 +1501,11 @@ impl StringLiteralFlags {
}
#[must_use]
pub fn with_triple_quotes(mut self) -> Self {
self.0 |= StringLiteralFlagsInner::TRIPLE_QUOTED;
pub fn with_triple_quotes(mut self, triple_quotes: TripleQuotes) -> Self {
self.0.set(
StringLiteralFlagsInner::TRIPLE_QUOTED,
triple_quotes.is_yes(),
);
self
}
@ -1550,8 +1577,12 @@ impl StringFlags for StringLiteralFlags {
/// Return `true` if the string is triple-quoted, i.e.,
/// it begins and ends with three consecutive quote characters.
/// For example: `"""bar"""`
fn is_triple_quoted(self) -> bool {
self.0.contains(StringLiteralFlagsInner::TRIPLE_QUOTED)
fn triple_quotes(self) -> TripleQuotes {
if self.0.contains(StringLiteralFlagsInner::TRIPLE_QUOTED) {
TripleQuotes::Yes
} else {
TripleQuotes::No
}
}
fn prefix(self) -> AnyStringPrefix {
@ -1866,8 +1897,11 @@ impl BytesLiteralFlags {
}
#[must_use]
pub fn with_triple_quotes(mut self) -> Self {
self.0 |= BytesLiteralFlagsInner::TRIPLE_QUOTED;
pub fn with_triple_quotes(mut self, triple_quotes: TripleQuotes) -> Self {
self.0.set(
BytesLiteralFlagsInner::TRIPLE_QUOTED,
triple_quotes.is_yes(),
);
self
}
@ -1910,8 +1944,12 @@ impl StringFlags for BytesLiteralFlags {
/// Return `true` if the bytestring is triple-quoted, i.e.,
/// it begins and ends with three consecutive quote characters.
/// For example: `b"""{bar}"""`
fn is_triple_quoted(self) -> bool {
self.0.contains(BytesLiteralFlagsInner::TRIPLE_QUOTED)
fn triple_quotes(self) -> TripleQuotes {
if self.0.contains(BytesLiteralFlagsInner::TRIPLE_QUOTED) {
TripleQuotes::Yes
} else {
TripleQuotes::No
}
}
/// Return the quoting style (single or double quotes)
@ -2035,7 +2073,7 @@ bitflags! {
}
}
#[derive(Default, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct AnyStringFlags(AnyStringFlagsInner);
impl AnyStringFlags {
@ -2073,13 +2111,11 @@ impl AnyStringFlags {
self
}
pub fn new(prefix: AnyStringPrefix, quotes: Quote, triple_quoted: bool) -> Self {
let new = Self::default().with_prefix(prefix).with_quote_style(quotes);
if triple_quoted {
new.with_triple_quotes()
} else {
new
}
pub fn new(prefix: AnyStringPrefix, quotes: Quote, triple_quotes: TripleQuotes) -> Self {
Self(AnyStringFlagsInner::empty())
.with_prefix(prefix)
.with_quote_style(quotes)
.with_triple_quotes(triple_quotes)
}
/// Does the string have a `u` or `U` prefix?
@ -2114,8 +2150,9 @@ impl AnyStringFlags {
}
#[must_use]
pub fn with_triple_quotes(mut self) -> Self {
self.0 |= AnyStringFlagsInner::TRIPLE_QUOTED;
pub fn with_triple_quotes(mut self, triple_quotes: TripleQuotes) -> Self {
self.0
.set(AnyStringFlagsInner::TRIPLE_QUOTED, triple_quotes.is_yes());
self
}
}
@ -2130,10 +2167,12 @@ impl StringFlags for AnyStringFlags {
}
}
/// Is the string triple-quoted, i.e.,
/// does it begin and end with three consecutive quote characters?
fn is_triple_quoted(self) -> bool {
self.0.contains(AnyStringFlagsInner::TRIPLE_QUOTED)
fn triple_quotes(self) -> TripleQuotes {
if self.0.contains(AnyStringFlagsInner::TRIPLE_QUOTED) {
TripleQuotes::Yes
} else {
TripleQuotes::No
}
}
fn prefix(self) -> AnyStringPrefix {
@ -2193,14 +2232,10 @@ impl From<AnyStringFlags> for StringLiteralFlags {
value.prefix()
)
};
let new = StringLiteralFlags::empty()
StringLiteralFlags::empty()
.with_quote_style(value.quote_style())
.with_prefix(prefix);
if value.is_triple_quoted() {
new.with_triple_quotes()
} else {
new
}
.with_prefix(prefix)
.with_triple_quotes(value.triple_quotes())
}
}
@ -2209,7 +2244,7 @@ impl From<StringLiteralFlags> for AnyStringFlags {
Self::new(
AnyStringPrefix::Regular(value.prefix()),
value.quote_style(),
value.is_triple_quoted(),
value.triple_quotes(),
)
}
}
@ -2222,14 +2257,10 @@ impl From<AnyStringFlags> for BytesLiteralFlags {
value.prefix()
)
};
let new = BytesLiteralFlags::empty()
BytesLiteralFlags::empty()
.with_quote_style(value.quote_style())
.with_prefix(bytestring_prefix);
if value.is_triple_quoted() {
new.with_triple_quotes()
} else {
new
}
.with_prefix(bytestring_prefix)
.with_triple_quotes(value.triple_quotes())
}
}
@ -2238,7 +2269,7 @@ impl From<BytesLiteralFlags> for AnyStringFlags {
Self::new(
AnyStringPrefix::Bytes(value.prefix()),
value.quote_style(),
value.is_triple_quoted(),
value.triple_quotes(),
)
}
}
@ -2251,14 +2282,10 @@ impl From<AnyStringFlags> for FStringFlags {
value.prefix()
)
};
let new = FStringFlags::empty()
FStringFlags::empty()
.with_quote_style(value.quote_style())
.with_prefix(fstring_prefix);
if value.is_triple_quoted() {
new.with_triple_quotes()
} else {
new
}
.with_prefix(fstring_prefix)
.with_triple_quotes(value.triple_quotes())
}
}
@ -2267,7 +2294,7 @@ impl From<FStringFlags> for AnyStringFlags {
Self::new(
AnyStringPrefix::Format(value.prefix()),
value.quote_style(),
value.is_triple_quoted(),
value.triple_quotes(),
)
}
}