mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-07 21:25:08 +00:00
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:
parent
9a33924a65
commit
b5e5271adf
16 changed files with 318 additions and 141 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2999,6 +2999,7 @@ dependencies = [
|
|||
"ruff_python_parser",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"test-case",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
use std::fmt::{self, Display, Formatter, Write};
|
||||
|
||||
use ruff_db::display::FormatterJoinExtension;
|
||||
use ruff_python_ast::str::Quote;
|
||||
use ruff_python_ast::str::{Quote, TripleQuotes};
|
||||
use ruff_python_literal::escape::AsciiEscape;
|
||||
|
||||
use crate::types::class_base::ClassBase;
|
||||
|
@ -98,7 +98,7 @@ impl Display for DisplayRepresentation<'_> {
|
|||
let escape =
|
||||
AsciiEscape::with_preferred_quote(bytes.value(self.db).as_ref(), Quote::Double);
|
||||
|
||||
escape.bytes_repr().write(f)
|
||||
escape.bytes_repr(TripleQuotes::No).write(f)
|
||||
}
|
||||
Type::SliceLiteral(slice) => {
|
||||
f.write_str("slice[")?;
|
||||
|
|
|
@ -97,3 +97,12 @@ b"TesT.WwW.ExamplE.CoM".split(b".")
|
|||
"hello\nworld".splitlines()
|
||||
"hello\nworld".splitlines(keepends=True)
|
||||
"hello\nworld".splitlines(keepends=False)
|
||||
|
||||
|
||||
# another positive demonstrating quote preservation
|
||||
"""
|
||||
"itemA"
|
||||
'itemB'
|
||||
'''itemC'''
|
||||
"'itemD'"
|
||||
""".split()
|
||||
|
|
|
@ -108,7 +108,9 @@ fn check_string_or_bytes(
|
|||
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryEscapedQuote, range);
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
flags.format_string_contents(&unescape_string(contents, opposite_quote_char)),
|
||||
flags
|
||||
.display_contents(&unescape_string(contents, opposite_quote_char))
|
||||
.to_string(),
|
||||
range,
|
||||
)));
|
||||
Some(diagnostic)
|
||||
|
|
|
@ -3,8 +3,8 @@ use std::cmp::Ordering;
|
|||
use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::{
|
||||
Expr, ExprCall, ExprContext, ExprList, ExprUnaryOp, StringLiteral, StringLiteralFlags,
|
||||
StringLiteralValue, UnaryOp,
|
||||
str::TripleQuotes, Expr, ExprCall, ExprContext, ExprList, ExprUnaryOp, StringLiteral,
|
||||
StringLiteralFlags, StringLiteralValue, UnaryOp,
|
||||
};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
|
@ -123,7 +123,17 @@ fn construct_replacement(elts: &[&str], flags: StringLiteralFlags) -> Expr {
|
|||
Expr::from(StringLiteral {
|
||||
value: Box::from(*elt),
|
||||
range: TextRange::default(),
|
||||
flags,
|
||||
// intentionally omit the triple quote flag, if set, to avoid strange
|
||||
// replacements like
|
||||
//
|
||||
// ```python
|
||||
// """
|
||||
// itemA
|
||||
// itemB
|
||||
// itemC
|
||||
// """.split() # -> ["""itemA""", """itemB""", """itemC"""]
|
||||
// ```
|
||||
flags: flags.with_triple_quotes(TripleQuotes::No),
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
|
|
|
@ -842,4 +842,29 @@ SIM905.py:72:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
|||
72 |+["a,b,c"] # ["a,b,c"]
|
||||
73 73 |
|
||||
74 74 | # negatives
|
||||
75 75 |
|
||||
75 75 |
|
||||
|
||||
SIM905.py:103:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
|
|
||||
102 | # another positive demonstrating quote preservation
|
||||
103 | / """
|
||||
104 | | "itemA"
|
||||
105 | | 'itemB'
|
||||
106 | | '''itemC'''
|
||||
107 | | "'itemD'"
|
||||
108 | | """.split()
|
||||
| |___________^ SIM905
|
||||
|
|
||||
= help: Replace with list literal
|
||||
|
||||
ℹ Safe fix
|
||||
100 100 |
|
||||
101 101 |
|
||||
102 102 | # another positive demonstrating quote preservation
|
||||
103 |-"""
|
||||
104 |-"itemA"
|
||||
105 |-'itemB'
|
||||
106 |-'''itemC'''
|
||||
107 |-"'itemD'"
|
||||
108 |-""".split()
|
||||
103 |+['"itemA"', "'itemB'", "'''itemC'''", "\"'itemD'\""]
|
||||
|
|
|
@ -405,7 +405,9 @@ pub(crate) fn printf_string_formatting(
|
|||
// Convert the `%`-format string to a `.format` string.
|
||||
format_strings.push((
|
||||
string_literal.range(),
|
||||
flags.format_string_contents(&percent_to_format(&format_string)),
|
||||
flags
|
||||
.display_contents(&percent_to_format(&format_string))
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,6 +68,24 @@ impl TryFrom<char> for Quote {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum TripleQuotes {
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
impl TripleQuotes {
|
||||
#[must_use]
|
||||
pub const fn is_yes(self) -> bool {
|
||||
matches!(self, Self::Yes)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn is_no(self) -> bool {
|
||||
matches!(self, Self::No)
|
||||
}
|
||||
}
|
||||
|
||||
/// Includes all permutations of `r`, `u`, `f`, and `fr` (`ur` is invalid, as is `uf`). This
|
||||
/// includes all possible orders, and all possible casings, for both single and triple quotes.
|
||||
///
|
||||
|
|
|
@ -20,6 +20,8 @@ ruff_python_parser = { workspace = true }
|
|||
ruff_source_file = { workspace = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
test-case = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
//! Generate Python source code from an abstract syntax tree (AST).
|
||||
|
||||
use std::fmt::Write;
|
||||
use std::ops::Deref;
|
||||
|
||||
use ruff_python_ast::str::Quote;
|
||||
use ruff_python_ast::{
|
||||
self as ast, Alias, ArgOrKeyword, BoolOp, CmpOp, Comprehension, ConversionFlag, DebugText,
|
||||
ExceptHandler, Expr, Identifier, MatchCase, Operator, Parameter, Parameters, Pattern,
|
||||
Singleton, Stmt, StringFlags, Suite, TypeParam, TypeParamParamSpec, TypeParamTypeVar,
|
||||
TypeParamTypeVarTuple, WithItem,
|
||||
self as ast, Alias, AnyStringFlags, ArgOrKeyword, BoolOp, BytesLiteralFlags, CmpOp,
|
||||
Comprehension, ConversionFlag, DebugText, ExceptHandler, Expr, FStringFlags, Identifier,
|
||||
MatchCase, Operator, Parameter, Parameters, Pattern, Singleton, Stmt, StringFlags, Suite,
|
||||
TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, WithItem,
|
||||
};
|
||||
use ruff_python_ast::{ParameterWithDefault, TypeParams};
|
||||
use ruff_python_literal::escape::{AsciiEscape, Escape, UnicodeEscape};
|
||||
|
@ -146,20 +146,44 @@ impl<'a> Generator<'a> {
|
|||
self.p(s.as_str());
|
||||
}
|
||||
|
||||
fn p_bytes_repr(&mut self, s: &[u8], quote: Quote) {
|
||||
let escape = AsciiEscape::with_preferred_quote(s, quote);
|
||||
fn p_bytes_repr(&mut self, s: &[u8], flags: BytesLiteralFlags) {
|
||||
// raw bytes are interpreted without escapes and should all be ascii (it's a python syntax
|
||||
// error otherwise), but if this assumption is violated, a `Utf8Error` will be returned from
|
||||
// `p_raw_bytes`, and we should fall back on the normal escaping behavior instead of
|
||||
// panicking
|
||||
if flags.prefix().is_raw() {
|
||||
if let Ok(s) = std::str::from_utf8(s) {
|
||||
write!(self.buffer, "{}", flags.display_contents(s))
|
||||
.expect("Writing to a String buffer should never fail");
|
||||
return;
|
||||
}
|
||||
}
|
||||
let escape = AsciiEscape::with_preferred_quote(s, flags.quote_style());
|
||||
if let Some(len) = escape.layout().len {
|
||||
self.buffer.reserve(len);
|
||||
}
|
||||
escape.bytes_repr().write(&mut self.buffer).unwrap(); // write to string doesn't fail
|
||||
escape
|
||||
.bytes_repr(flags.triple_quotes())
|
||||
.write(&mut self.buffer)
|
||||
.expect("Writing to a String buffer should never fail");
|
||||
}
|
||||
|
||||
fn p_str_repr(&mut self, s: &str, quote: Quote) {
|
||||
let escape = UnicodeEscape::with_preferred_quote(s, quote);
|
||||
fn p_str_repr(&mut self, s: &str, flags: impl Into<AnyStringFlags>) {
|
||||
let flags = flags.into();
|
||||
if flags.prefix().is_raw() {
|
||||
write!(self.buffer, "{}", flags.display_contents(s))
|
||||
.expect("Writing to a String buffer should never fail");
|
||||
return;
|
||||
}
|
||||
self.p(flags.prefix().as_str());
|
||||
let escape = UnicodeEscape::with_preferred_quote(s, flags.quote_style());
|
||||
if let Some(len) = escape.layout().len {
|
||||
self.buffer.reserve(len);
|
||||
}
|
||||
escape.str_repr().write(&mut self.buffer).unwrap(); // write to string doesn't fail
|
||||
escape
|
||||
.str_repr(flags.triple_quotes())
|
||||
.write(&mut self.buffer)
|
||||
.expect("Writing to a String buffer should never fail");
|
||||
}
|
||||
|
||||
fn p_if(&mut self, cond: bool, s: &str) {
|
||||
|
@ -1093,7 +1117,7 @@ impl<'a> Generator<'a> {
|
|||
let mut first = true;
|
||||
for bytes_literal in value {
|
||||
self.p_delim(&mut first, " ");
|
||||
self.p_bytes_repr(&bytes_literal.value, bytes_literal.flags.quote_style());
|
||||
self.p_bytes_repr(&bytes_literal.value, bytes_literal.flags);
|
||||
}
|
||||
}
|
||||
Expr::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => {
|
||||
|
@ -1280,19 +1304,7 @@ impl<'a> Generator<'a> {
|
|||
|
||||
fn unparse_string_literal(&mut self, string_literal: &ast::StringLiteral) {
|
||||
let ast::StringLiteral { value, flags, .. } = string_literal;
|
||||
// for raw strings, we don't want to perform the UnicodeEscape in `p_str_repr`, so build the
|
||||
// replacement here
|
||||
if flags.prefix().is_raw() {
|
||||
self.p(flags.prefix().as_str());
|
||||
self.p(flags.quote_str());
|
||||
self.p(value);
|
||||
self.p(flags.quote_str());
|
||||
} else {
|
||||
if flags.prefix().is_unicode() {
|
||||
self.p("u");
|
||||
}
|
||||
self.p_str_repr(value, flags.quote_style());
|
||||
}
|
||||
self.p_str_repr(value, *flags);
|
||||
}
|
||||
|
||||
fn unparse_string_literal_value(&mut self, value: &ast::StringLiteralValue) {
|
||||
|
@ -1312,7 +1324,7 @@ impl<'a> Generator<'a> {
|
|||
self.unparse_string_literal(string_literal);
|
||||
}
|
||||
ast::FStringPart::FString(f_string) => {
|
||||
self.unparse_f_string(&f_string.elements, f_string.flags.quote_style());
|
||||
self.unparse_f_string(&f_string.elements, f_string.flags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1396,12 +1408,11 @@ impl<'a> Generator<'a> {
|
|||
|
||||
/// Unparse `values` with [`Generator::unparse_f_string_body`], using `quote` as the preferred
|
||||
/// surrounding quote style.
|
||||
fn unparse_f_string(&mut self, values: &[ast::FStringElement], quote: Quote) {
|
||||
self.p("f");
|
||||
fn unparse_f_string(&mut self, values: &[ast::FStringElement], flags: FStringFlags) {
|
||||
let mut generator = Generator::new(self.indent, self.line_ending);
|
||||
generator.unparse_f_string_body(values);
|
||||
let body = &generator.buffer;
|
||||
self.p_str_repr(body, quote);
|
||||
self.p_str_repr(body, flags);
|
||||
}
|
||||
|
||||
fn unparse_alias(&mut self, alias: &Alias) {
|
||||
|
@ -1724,10 +1735,53 @@ class Foo:
|
|||
assert_round_trip!(r#"f"hello""#);
|
||||
assert_eq!(round_trip(r#"("abc" "def" "ghi")"#), r#""abc" "def" "ghi""#);
|
||||
assert_eq!(round_trip(r#""he\"llo""#), r#"'he"llo'"#);
|
||||
assert_eq!(round_trip(r#"b"he\"llo""#), r#"b'he"llo'"#);
|
||||
assert_eq!(round_trip(r#"f"abc{'def'}{1}""#), r#"f"abc{'def'}{1}""#);
|
||||
assert_round_trip!(r#"f'abc{"def"}{1}'"#);
|
||||
}
|
||||
|
||||
/// test all of the valid string literal prefix and quote combinations from
|
||||
/// https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals
|
||||
///
|
||||
/// Note that the numeric ids on the input/output and quote fields prevent name conflicts from
|
||||
/// the test_matrix but are otherwise unnecessary
|
||||
#[test_case::test_matrix(
|
||||
[
|
||||
("r", "r", 0),
|
||||
("u", "u", 1),
|
||||
("R", "R", 2),
|
||||
("U", "u", 3), // case not tracked
|
||||
("f", "f", 4),
|
||||
("F", "f", 5), // f case not tracked
|
||||
("fr", "rf", 6), // r before f
|
||||
("Fr", "rf", 7), // f case not tracked, r before f
|
||||
("fR", "Rf", 8), // r before f
|
||||
("FR", "Rf", 9), // f case not tracked, r before f
|
||||
("rf", "rf", 10),
|
||||
("rF", "rf", 11), // f case not tracked
|
||||
("Rf", "Rf", 12),
|
||||
("RF", "Rf", 13), // f case not tracked
|
||||
// bytestrings
|
||||
("b", "b", 14),
|
||||
("B", "b", 15), // b case
|
||||
("br", "rb", 16), // r before b
|
||||
("Br", "rb", 17), // b case, r before b
|
||||
("bR", "Rb", 18), // r before b
|
||||
("BR", "Rb", 19), // b case, r before b
|
||||
("rb", "rb", 20),
|
||||
("rB", "rb", 21), // b case
|
||||
("Rb", "Rb", 22),
|
||||
("RB", "Rb", 23), // b case
|
||||
],
|
||||
[("\"", 0), ("'",1), ("\"\"\"", 2), ("'''", 3)],
|
||||
["hello", "{hello} {world}"]
|
||||
)]
|
||||
fn prefix_quotes((inp, out, _id): (&str, &str, u8), (quote, _id2): (&str, u8), base: &str) {
|
||||
let input = format!("{inp}{quote}{base}{quote}");
|
||||
let output = format!("{out}{quote}{base}{quote}");
|
||||
assert_eq!(round_trip(&input), output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw() {
|
||||
assert_round_trip!(r#"r"a\.b""#); // https://github.com/astral-sh/ruff/issues/9663
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use ruff_python_ast::str::Quote;
|
||||
use ruff_python_ast::{
|
||||
str::{Quote, TripleQuotes},
|
||||
BytesLiteralFlags, StringFlags, StringLiteralFlags,
|
||||
};
|
||||
|
||||
pub struct EscapeLayout {
|
||||
pub quote: Quote,
|
||||
|
@ -60,23 +63,32 @@ impl<'a> UnicodeEscape<'a> {
|
|||
Self::with_preferred_quote(source, Quote::Single)
|
||||
}
|
||||
#[inline]
|
||||
pub fn str_repr<'r>(&'a self) -> StrRepr<'r, 'a> {
|
||||
StrRepr(self)
|
||||
pub fn str_repr<'r>(&'a self, triple_quotes: TripleQuotes) -> StrRepr<'r, 'a> {
|
||||
StrRepr {
|
||||
escape: self,
|
||||
triple_quotes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StrRepr<'r, 'a>(&'r UnicodeEscape<'a>);
|
||||
pub struct StrRepr<'r, 'a> {
|
||||
escape: &'r UnicodeEscape<'a>,
|
||||
triple_quotes: TripleQuotes,
|
||||
}
|
||||
|
||||
impl StrRepr<'_, '_> {
|
||||
pub fn write(&self, formatter: &mut impl std::fmt::Write) -> std::fmt::Result {
|
||||
let quote = self.0.layout().quote.as_char();
|
||||
formatter.write_char(quote)?;
|
||||
self.0.write_body(formatter)?;
|
||||
formatter.write_char(quote)
|
||||
let flags = StringLiteralFlags::empty()
|
||||
.with_quote_style(self.escape.layout().quote)
|
||||
.with_triple_quotes(self.triple_quotes);
|
||||
formatter.write_str(flags.quote_str())?;
|
||||
self.escape.write_body(formatter)?;
|
||||
formatter.write_str(flags.quote_str())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> Option<String> {
|
||||
let mut s = String::with_capacity(self.0.layout().len?);
|
||||
let mut s = String::with_capacity(self.escape.layout().len?);
|
||||
self.write(&mut s).unwrap();
|
||||
Some(s)
|
||||
}
|
||||
|
@ -244,8 +256,11 @@ impl<'a> AsciiEscape<'a> {
|
|||
Self::with_preferred_quote(source, Quote::Single)
|
||||
}
|
||||
#[inline]
|
||||
pub fn bytes_repr<'r>(&'a self) -> BytesRepr<'r, 'a> {
|
||||
BytesRepr(self)
|
||||
pub fn bytes_repr<'r>(&'a self, triple_quotes: TripleQuotes) -> BytesRepr<'r, 'a> {
|
||||
BytesRepr {
|
||||
escape: self,
|
||||
triple_quotes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -360,19 +375,26 @@ impl Escape for AsciiEscape<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct BytesRepr<'r, 'a>(&'r AsciiEscape<'a>);
|
||||
pub struct BytesRepr<'r, 'a> {
|
||||
escape: &'r AsciiEscape<'a>,
|
||||
triple_quotes: TripleQuotes,
|
||||
}
|
||||
|
||||
impl BytesRepr<'_, '_> {
|
||||
pub fn write(&self, formatter: &mut impl std::fmt::Write) -> std::fmt::Result {
|
||||
let quote = self.0.layout().quote.as_char();
|
||||
let flags = BytesLiteralFlags::empty()
|
||||
.with_quote_style(self.escape.layout().quote)
|
||||
.with_triple_quotes(self.triple_quotes);
|
||||
|
||||
formatter.write_char('b')?;
|
||||
formatter.write_char(quote)?;
|
||||
self.0.write_body(formatter)?;
|
||||
formatter.write_char(quote)
|
||||
formatter.write_str(flags.quote_str())?;
|
||||
self.escape.write_body(formatter)?;
|
||||
formatter.write_str(flags.quote_str())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> Option<String> {
|
||||
let mut s = String::with_capacity(self.0.layout().len?);
|
||||
let mut s = String::with_capacity(self.escape.layout().len?);
|
||||
self.write(&mut s).unwrap();
|
||||
Some(s)
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ use std::fmt;
|
|||
use bitflags::bitflags;
|
||||
|
||||
use ruff_python_ast::name::Name;
|
||||
use ruff_python_ast::str::Quote;
|
||||
use ruff_python_ast::str::{Quote, TripleQuotes};
|
||||
use ruff_python_ast::str_prefix::{
|
||||
AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix,
|
||||
};
|
||||
|
@ -718,8 +718,12 @@ impl StringFlags for TokenFlags {
|
|||
}
|
||||
}
|
||||
|
||||
fn is_triple_quoted(self) -> bool {
|
||||
self.intersects(TokenFlags::TRIPLE_QUOTED_STRING)
|
||||
fn triple_quotes(self) -> TripleQuotes {
|
||||
if self.intersects(TokenFlags::TRIPLE_QUOTED_STRING) {
|
||||
TripleQuotes::Yes
|
||||
} else {
|
||||
TripleQuotes::No
|
||||
}
|
||||
}
|
||||
|
||||
fn prefix(self) -> AnyStringPrefix {
|
||||
|
@ -769,7 +773,7 @@ impl TokenFlags {
|
|||
|
||||
/// Converts this type to [`AnyStringFlags`], setting the equivalent flags.
|
||||
pub(crate) fn as_any_string_flags(self) -> AnyStringFlags {
|
||||
AnyStringFlags::new(self.prefix(), self.quote_style(), self.is_triple_quoted())
|
||||
AnyStringFlags::new(self.prefix(), self.quote_style(), self.triple_quotes())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue