mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-31 07:37:38 +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_python_parser",
|
||||||
"ruff_source_file",
|
"ruff_source_file",
|
||||||
"ruff_text_size",
|
"ruff_text_size",
|
||||||
|
"test-case",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
use std::fmt::{self, Display, Formatter, Write};
|
use std::fmt::{self, Display, Formatter, Write};
|
||||||
|
|
||||||
use ruff_db::display::FormatterJoinExtension;
|
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 ruff_python_literal::escape::AsciiEscape;
|
||||||
|
|
||||||
use crate::types::class_base::ClassBase;
|
use crate::types::class_base::ClassBase;
|
||||||
|
@ -98,7 +98,7 @@ impl Display for DisplayRepresentation<'_> {
|
||||||
let escape =
|
let escape =
|
||||||
AsciiEscape::with_preferred_quote(bytes.value(self.db).as_ref(), Quote::Double);
|
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) => {
|
Type::SliceLiteral(slice) => {
|
||||||
f.write_str("slice[")?;
|
f.write_str("slice[")?;
|
||||||
|
|
|
@ -97,3 +97,12 @@ b"TesT.WwW.ExamplE.CoM".split(b".")
|
||||||
"hello\nworld".splitlines()
|
"hello\nworld".splitlines()
|
||||||
"hello\nworld".splitlines(keepends=True)
|
"hello\nworld".splitlines(keepends=True)
|
||||||
"hello\nworld".splitlines(keepends=False)
|
"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);
|
let mut diagnostic = Diagnostic::new(UnnecessaryEscapedQuote, range);
|
||||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
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,
|
range,
|
||||||
)));
|
)));
|
||||||
Some(diagnostic)
|
Some(diagnostic)
|
||||||
|
|
|
@ -3,8 +3,8 @@ use std::cmp::Ordering;
|
||||||
use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation};
|
use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||||
use ruff_python_ast::{
|
use ruff_python_ast::{
|
||||||
Expr, ExprCall, ExprContext, ExprList, ExprUnaryOp, StringLiteral, StringLiteralFlags,
|
str::TripleQuotes, Expr, ExprCall, ExprContext, ExprList, ExprUnaryOp, StringLiteral,
|
||||||
StringLiteralValue, UnaryOp,
|
StringLiteralFlags, StringLiteralValue, UnaryOp,
|
||||||
};
|
};
|
||||||
use ruff_text_size::{Ranged, TextRange};
|
use ruff_text_size::{Ranged, TextRange};
|
||||||
|
|
||||||
|
@ -123,7 +123,17 @@ fn construct_replacement(elts: &[&str], flags: StringLiteralFlags) -> Expr {
|
||||||
Expr::from(StringLiteral {
|
Expr::from(StringLiteral {
|
||||||
value: Box::from(*elt),
|
value: Box::from(*elt),
|
||||||
range: TextRange::default(),
|
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(),
|
.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"]
|
72 |+["a,b,c"] # ["a,b,c"]
|
||||||
73 73 |
|
73 73 |
|
||||||
74 74 | # negatives
|
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.
|
// Convert the `%`-format string to a `.format` string.
|
||||||
format_strings.push((
|
format_strings.push((
|
||||||
string_literal.range(),
|
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 ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||||
|
|
||||||
use crate::name::Name;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
int,
|
int,
|
||||||
str::Quote,
|
name::Name,
|
||||||
|
str::{Quote, TripleQuotes},
|
||||||
str_prefix::{AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix},
|
str_prefix::{AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix},
|
||||||
ExceptHandler, Expr, FStringElement, LiteralExpressionRef, Pattern, Stmt, TypeParam,
|
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?
|
/// Does the string use single or double quotes in its opener and closer?
|
||||||
fn quote_style(self) -> Quote;
|
fn quote_style(self) -> Quote;
|
||||||
|
|
||||||
/// Is the string triple-quoted, i.e.,
|
fn triple_quotes(self) -> TripleQuotes;
|
||||||
/// does it begin and end with three consecutive quote characters?
|
|
||||||
fn is_triple_quoted(self) -> bool;
|
|
||||||
|
|
||||||
fn prefix(self) -> AnyStringPrefix;
|
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.
|
/// A `str` representation of the quotes used to start and close.
|
||||||
/// This does not include any prefixes the string has in its opener.
|
/// This does not include any prefixes the string has in its opener.
|
||||||
fn quote_str(self) -> &'static str {
|
fn quote_str(self) -> &'static str {
|
||||||
if self.is_triple_quoted() {
|
match (self.triple_quotes(), self.quote_style()) {
|
||||||
match self.quote_style() {
|
(TripleQuotes::Yes, Quote::Single) => "'''",
|
||||||
Quote::Single => "'''",
|
(TripleQuotes::Yes, Quote::Double) => r#"""""#,
|
||||||
Quote::Double => r#"""""#,
|
(TripleQuotes::No, Quote::Single) => "'",
|
||||||
}
|
(TripleQuotes::No, Quote::Double) => "\"",
|
||||||
} else {
|
|
||||||
match self.quote_style() {
|
|
||||||
Quote::Single => "'",
|
|
||||||
Quote::Double => "\"",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1028,10 +1027,30 @@ pub trait StringFlags: Copy {
|
||||||
self.quote_len()
|
self.quote_len()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_string_contents(self, contents: &str) -> String {
|
fn display_contents(self, contents: &str) -> DisplayFlags {
|
||||||
let prefix = self.prefix();
|
DisplayFlags {
|
||||||
let quote_str = self.quote_str();
|
prefix: self.prefix(),
|
||||||
format!("{prefix}{quote_str}{contents}{quote_str}")
|
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]
|
#[must_use]
|
||||||
pub fn with_triple_quotes(mut self) -> Self {
|
pub fn with_triple_quotes(mut self, triple_quotes: TripleQuotes) -> Self {
|
||||||
self.0 |= FStringFlagsInner::TRIPLE_QUOTED;
|
self.0
|
||||||
|
.set(FStringFlagsInner::TRIPLE_QUOTED, triple_quotes.is_yes());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1132,8 +1152,12 @@ impl StringFlags for FStringFlags {
|
||||||
/// Return `true` if the f-string is triple-quoted, i.e.,
|
/// Return `true` if the f-string is triple-quoted, i.e.,
|
||||||
/// it begins and ends with three consecutive quote characters.
|
/// it begins and ends with three consecutive quote characters.
|
||||||
/// For example: `f"""{bar}"""`
|
/// For example: `f"""{bar}"""`
|
||||||
fn is_triple_quoted(self) -> bool {
|
fn triple_quotes(self) -> TripleQuotes {
|
||||||
self.0.contains(FStringFlagsInner::TRIPLE_QUOTED)
|
if self.0.contains(FStringFlagsInner::TRIPLE_QUOTED) {
|
||||||
|
TripleQuotes::Yes
|
||||||
|
} else {
|
||||||
|
TripleQuotes::No
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the quoting style (single or double quotes)
|
/// Return the quoting style (single or double quotes)
|
||||||
|
@ -1477,8 +1501,11 @@ impl StringLiteralFlags {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_triple_quotes(mut self) -> Self {
|
pub fn with_triple_quotes(mut self, triple_quotes: TripleQuotes) -> Self {
|
||||||
self.0 |= StringLiteralFlagsInner::TRIPLE_QUOTED;
|
self.0.set(
|
||||||
|
StringLiteralFlagsInner::TRIPLE_QUOTED,
|
||||||
|
triple_quotes.is_yes(),
|
||||||
|
);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1550,8 +1577,12 @@ impl StringFlags for StringLiteralFlags {
|
||||||
/// Return `true` if the string is triple-quoted, i.e.,
|
/// Return `true` if the string is triple-quoted, i.e.,
|
||||||
/// it begins and ends with three consecutive quote characters.
|
/// it begins and ends with three consecutive quote characters.
|
||||||
/// For example: `"""bar"""`
|
/// For example: `"""bar"""`
|
||||||
fn is_triple_quoted(self) -> bool {
|
fn triple_quotes(self) -> TripleQuotes {
|
||||||
self.0.contains(StringLiteralFlagsInner::TRIPLE_QUOTED)
|
if self.0.contains(StringLiteralFlagsInner::TRIPLE_QUOTED) {
|
||||||
|
TripleQuotes::Yes
|
||||||
|
} else {
|
||||||
|
TripleQuotes::No
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prefix(self) -> AnyStringPrefix {
|
fn prefix(self) -> AnyStringPrefix {
|
||||||
|
@ -1866,8 +1897,11 @@ impl BytesLiteralFlags {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_triple_quotes(mut self) -> Self {
|
pub fn with_triple_quotes(mut self, triple_quotes: TripleQuotes) -> Self {
|
||||||
self.0 |= BytesLiteralFlagsInner::TRIPLE_QUOTED;
|
self.0.set(
|
||||||
|
BytesLiteralFlagsInner::TRIPLE_QUOTED,
|
||||||
|
triple_quotes.is_yes(),
|
||||||
|
);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1910,8 +1944,12 @@ impl StringFlags for BytesLiteralFlags {
|
||||||
/// Return `true` if the bytestring is triple-quoted, i.e.,
|
/// Return `true` if the bytestring is triple-quoted, i.e.,
|
||||||
/// it begins and ends with three consecutive quote characters.
|
/// it begins and ends with three consecutive quote characters.
|
||||||
/// For example: `b"""{bar}"""`
|
/// For example: `b"""{bar}"""`
|
||||||
fn is_triple_quoted(self) -> bool {
|
fn triple_quotes(self) -> TripleQuotes {
|
||||||
self.0.contains(BytesLiteralFlagsInner::TRIPLE_QUOTED)
|
if self.0.contains(BytesLiteralFlagsInner::TRIPLE_QUOTED) {
|
||||||
|
TripleQuotes::Yes
|
||||||
|
} else {
|
||||||
|
TripleQuotes::No
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the quoting style (single or double quotes)
|
/// 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);
|
pub struct AnyStringFlags(AnyStringFlagsInner);
|
||||||
|
|
||||||
impl AnyStringFlags {
|
impl AnyStringFlags {
|
||||||
|
@ -2073,13 +2111,11 @@ impl AnyStringFlags {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(prefix: AnyStringPrefix, quotes: Quote, triple_quoted: bool) -> Self {
|
pub fn new(prefix: AnyStringPrefix, quotes: Quote, triple_quotes: TripleQuotes) -> Self {
|
||||||
let new = Self::default().with_prefix(prefix).with_quote_style(quotes);
|
Self(AnyStringFlagsInner::empty())
|
||||||
if triple_quoted {
|
.with_prefix(prefix)
|
||||||
new.with_triple_quotes()
|
.with_quote_style(quotes)
|
||||||
} else {
|
.with_triple_quotes(triple_quotes)
|
||||||
new
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Does the string have a `u` or `U` prefix?
|
/// Does the string have a `u` or `U` prefix?
|
||||||
|
@ -2114,8 +2150,9 @@ impl AnyStringFlags {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_triple_quotes(mut self) -> Self {
|
pub fn with_triple_quotes(mut self, triple_quotes: TripleQuotes) -> Self {
|
||||||
self.0 |= AnyStringFlagsInner::TRIPLE_QUOTED;
|
self.0
|
||||||
|
.set(AnyStringFlagsInner::TRIPLE_QUOTED, triple_quotes.is_yes());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2130,10 +2167,12 @@ impl StringFlags for AnyStringFlags {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Is the string triple-quoted, i.e.,
|
fn triple_quotes(self) -> TripleQuotes {
|
||||||
/// does it begin and end with three consecutive quote characters?
|
if self.0.contains(AnyStringFlagsInner::TRIPLE_QUOTED) {
|
||||||
fn is_triple_quoted(self) -> bool {
|
TripleQuotes::Yes
|
||||||
self.0.contains(AnyStringFlagsInner::TRIPLE_QUOTED)
|
} else {
|
||||||
|
TripleQuotes::No
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prefix(self) -> AnyStringPrefix {
|
fn prefix(self) -> AnyStringPrefix {
|
||||||
|
@ -2193,14 +2232,10 @@ impl From<AnyStringFlags> for StringLiteralFlags {
|
||||||
value.prefix()
|
value.prefix()
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let new = StringLiteralFlags::empty()
|
StringLiteralFlags::empty()
|
||||||
.with_quote_style(value.quote_style())
|
.with_quote_style(value.quote_style())
|
||||||
.with_prefix(prefix);
|
.with_prefix(prefix)
|
||||||
if value.is_triple_quoted() {
|
.with_triple_quotes(value.triple_quotes())
|
||||||
new.with_triple_quotes()
|
|
||||||
} else {
|
|
||||||
new
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2209,7 +2244,7 @@ impl From<StringLiteralFlags> for AnyStringFlags {
|
||||||
Self::new(
|
Self::new(
|
||||||
AnyStringPrefix::Regular(value.prefix()),
|
AnyStringPrefix::Regular(value.prefix()),
|
||||||
value.quote_style(),
|
value.quote_style(),
|
||||||
value.is_triple_quoted(),
|
value.triple_quotes(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2222,14 +2257,10 @@ impl From<AnyStringFlags> for BytesLiteralFlags {
|
||||||
value.prefix()
|
value.prefix()
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let new = BytesLiteralFlags::empty()
|
BytesLiteralFlags::empty()
|
||||||
.with_quote_style(value.quote_style())
|
.with_quote_style(value.quote_style())
|
||||||
.with_prefix(bytestring_prefix);
|
.with_prefix(bytestring_prefix)
|
||||||
if value.is_triple_quoted() {
|
.with_triple_quotes(value.triple_quotes())
|
||||||
new.with_triple_quotes()
|
|
||||||
} else {
|
|
||||||
new
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2238,7 +2269,7 @@ impl From<BytesLiteralFlags> for AnyStringFlags {
|
||||||
Self::new(
|
Self::new(
|
||||||
AnyStringPrefix::Bytes(value.prefix()),
|
AnyStringPrefix::Bytes(value.prefix()),
|
||||||
value.quote_style(),
|
value.quote_style(),
|
||||||
value.is_triple_quoted(),
|
value.triple_quotes(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2251,14 +2282,10 @@ impl From<AnyStringFlags> for FStringFlags {
|
||||||
value.prefix()
|
value.prefix()
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let new = FStringFlags::empty()
|
FStringFlags::empty()
|
||||||
.with_quote_style(value.quote_style())
|
.with_quote_style(value.quote_style())
|
||||||
.with_prefix(fstring_prefix);
|
.with_prefix(fstring_prefix)
|
||||||
if value.is_triple_quoted() {
|
.with_triple_quotes(value.triple_quotes())
|
||||||
new.with_triple_quotes()
|
|
||||||
} else {
|
|
||||||
new
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2267,7 +2294,7 @@ impl From<FStringFlags> for AnyStringFlags {
|
||||||
Self::new(
|
Self::new(
|
||||||
AnyStringPrefix::Format(value.prefix()),
|
AnyStringPrefix::Format(value.prefix()),
|
||||||
value.quote_style(),
|
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 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.
|
/// 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_source_file = { workspace = true }
|
||||||
ruff_text_size = { workspace = true }
|
ruff_text_size = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
test-case = { workspace = true }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
//! Generate Python source code from an abstract syntax tree (AST).
|
//! Generate Python source code from an abstract syntax tree (AST).
|
||||||
|
|
||||||
|
use std::fmt::Write;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
use ruff_python_ast::str::Quote;
|
|
||||||
use ruff_python_ast::{
|
use ruff_python_ast::{
|
||||||
self as ast, Alias, ArgOrKeyword, BoolOp, CmpOp, Comprehension, ConversionFlag, DebugText,
|
self as ast, Alias, AnyStringFlags, ArgOrKeyword, BoolOp, BytesLiteralFlags, CmpOp,
|
||||||
ExceptHandler, Expr, Identifier, MatchCase, Operator, Parameter, Parameters, Pattern,
|
Comprehension, ConversionFlag, DebugText, ExceptHandler, Expr, FStringFlags, Identifier,
|
||||||
Singleton, Stmt, StringFlags, Suite, TypeParam, TypeParamParamSpec, TypeParamTypeVar,
|
MatchCase, Operator, Parameter, Parameters, Pattern, Singleton, Stmt, StringFlags, Suite,
|
||||||
TypeParamTypeVarTuple, WithItem,
|
TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, WithItem,
|
||||||
};
|
};
|
||||||
use ruff_python_ast::{ParameterWithDefault, TypeParams};
|
use ruff_python_ast::{ParameterWithDefault, TypeParams};
|
||||||
use ruff_python_literal::escape::{AsciiEscape, Escape, UnicodeEscape};
|
use ruff_python_literal::escape::{AsciiEscape, Escape, UnicodeEscape};
|
||||||
|
@ -146,20 +146,44 @@ impl<'a> Generator<'a> {
|
||||||
self.p(s.as_str());
|
self.p(s.as_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn p_bytes_repr(&mut self, s: &[u8], quote: Quote) {
|
fn p_bytes_repr(&mut self, s: &[u8], flags: BytesLiteralFlags) {
|
||||||
let escape = AsciiEscape::with_preferred_quote(s, quote);
|
// 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 {
|
if let Some(len) = escape.layout().len {
|
||||||
self.buffer.reserve(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) {
|
fn p_str_repr(&mut self, s: &str, flags: impl Into<AnyStringFlags>) {
|
||||||
let escape = UnicodeEscape::with_preferred_quote(s, quote);
|
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 {
|
if let Some(len) = escape.layout().len {
|
||||||
self.buffer.reserve(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) {
|
fn p_if(&mut self, cond: bool, s: &str) {
|
||||||
|
@ -1093,7 +1117,7 @@ impl<'a> Generator<'a> {
|
||||||
let mut first = true;
|
let mut first = true;
|
||||||
for bytes_literal in value {
|
for bytes_literal in value {
|
||||||
self.p_delim(&mut first, " ");
|
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, .. }) => {
|
Expr::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => {
|
||||||
|
@ -1280,19 +1304,7 @@ impl<'a> Generator<'a> {
|
||||||
|
|
||||||
fn unparse_string_literal(&mut self, string_literal: &ast::StringLiteral) {
|
fn unparse_string_literal(&mut self, string_literal: &ast::StringLiteral) {
|
||||||
let ast::StringLiteral { value, flags, .. } = string_literal;
|
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
|
self.p_str_repr(value, *flags);
|
||||||
// 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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unparse_string_literal_value(&mut self, value: &ast::StringLiteralValue) {
|
fn unparse_string_literal_value(&mut self, value: &ast::StringLiteralValue) {
|
||||||
|
@ -1312,7 +1324,7 @@ impl<'a> Generator<'a> {
|
||||||
self.unparse_string_literal(string_literal);
|
self.unparse_string_literal(string_literal);
|
||||||
}
|
}
|
||||||
ast::FStringPart::FString(f_string) => {
|
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
|
/// Unparse `values` with [`Generator::unparse_f_string_body`], using `quote` as the preferred
|
||||||
/// surrounding quote style.
|
/// surrounding quote style.
|
||||||
fn unparse_f_string(&mut self, values: &[ast::FStringElement], quote: Quote) {
|
fn unparse_f_string(&mut self, values: &[ast::FStringElement], flags: FStringFlags) {
|
||||||
self.p("f");
|
|
||||||
let mut generator = Generator::new(self.indent, self.line_ending);
|
let mut generator = Generator::new(self.indent, self.line_ending);
|
||||||
generator.unparse_f_string_body(values);
|
generator.unparse_f_string_body(values);
|
||||||
let body = &generator.buffer;
|
let body = &generator.buffer;
|
||||||
self.p_str_repr(body, quote);
|
self.p_str_repr(body, flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unparse_alias(&mut self, alias: &Alias) {
|
fn unparse_alias(&mut self, alias: &Alias) {
|
||||||
|
@ -1724,10 +1735,53 @@ class Foo:
|
||||||
assert_round_trip!(r#"f"hello""#);
|
assert_round_trip!(r#"f"hello""#);
|
||||||
assert_eq!(round_trip(r#"("abc" "def" "ghi")"#), r#""abc" "def" "ghi""#);
|
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#""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_eq!(round_trip(r#"f"abc{'def'}{1}""#), r#"f"abc{'def'}{1}""#);
|
||||||
assert_round_trip!(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]
|
#[test]
|
||||||
fn raw() {
|
fn raw() {
|
||||||
assert_round_trip!(r#"r"a\.b""#); // https://github.com/astral-sh/ruff/issues/9663
|
assert_round_trip!(r#"r"a\.b""#); // https://github.com/astral-sh/ruff/issues/9663
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use ruff_formatter::{format_args, write, FormatContext};
|
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::{
|
use ruff_python_ast::str_prefix::{
|
||||||
AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix,
|
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() {
|
if !string.is_implicit_concatenated() {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use memchr::memchr2;
|
use memchr::memchr2;
|
||||||
pub(crate) use normalize::{normalize_string, NormalizedString, StringNormalizer};
|
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::StringLikePart;
|
||||||
use ruff_python_ast::{
|
use ruff_python_ast::{
|
||||||
self as ast,
|
self as ast,
|
||||||
|
@ -95,11 +95,11 @@ impl StringLikeExtensions for ast::StringLike<'_> {
|
||||||
fn contains_line_break_or_comments(
|
fn contains_line_break_or_comments(
|
||||||
elements: &ast::FStringElements,
|
elements: &ast::FStringElements,
|
||||||
context: &PyFormatContext,
|
context: &PyFormatContext,
|
||||||
is_triple_quoted: bool,
|
triple_quotes: TripleQuotes,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
elements.iter().any(|element| match element {
|
elements.iter().any(|element| match element {
|
||||||
ast::FStringElement::Literal(literal) => {
|
ast::FStringElement::Literal(literal) => {
|
||||||
is_triple_quoted
|
triple_quotes.is_yes()
|
||||||
&& context.source().contains_line_break(literal.range())
|
&& context.source().contains_line_break(literal.range())
|
||||||
}
|
}
|
||||||
ast::FStringElement::Expression(expression) => {
|
ast::FStringElement::Expression(expression) => {
|
||||||
|
@ -119,7 +119,7 @@ impl StringLikeExtensions for ast::StringLike<'_> {
|
||||||
contains_line_break_or_comments(
|
contains_line_break_or_comments(
|
||||||
&spec.elements,
|
&spec.elements,
|
||||||
context,
|
context,
|
||||||
is_triple_quoted,
|
triple_quotes,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|| expression.debug_text.as_ref().is_some_and(|debug_text| {
|
|| expression.debug_text.as_ref().is_some_and(|debug_text| {
|
||||||
|
@ -134,7 +134,7 @@ impl StringLikeExtensions for ast::StringLike<'_> {
|
||||||
contains_line_break_or_comments(
|
contains_line_break_or_comments(
|
||||||
&f_string.elements,
|
&f_string.elements,
|
||||||
context,
|
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_formatter::FormatContext;
|
||||||
use ruff_python_ast::visitor::source_order::SourceOrderVisitor;
|
use ruff_python_ast::visitor::source_order::SourceOrderVisitor;
|
||||||
use ruff_python_ast::{
|
use ruff_python_ast::{
|
||||||
str::Quote, AnyStringFlags, BytesLiteral, FString, FStringElement, FStringElements,
|
str::{Quote, TripleQuotes},
|
||||||
FStringFlags, StringFlags, StringLikePart, StringLiteral,
|
AnyStringFlags, BytesLiteral, FString, FStringElement, FStringElements, FStringFlags,
|
||||||
|
StringFlags, StringLikePart, StringLiteral,
|
||||||
};
|
};
|
||||||
use ruff_text_size::{Ranged, TextRange, TextSlice};
|
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 {
|
pub(crate) fn from_str(text: &str, flags: AnyStringFlags, preferred_quote: Quote) -> Self {
|
||||||
let kind = if flags.is_raw_string() {
|
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() {
|
} else if flags.is_triple_quoted() {
|
||||||
QuoteMetadataKind::triple_quoted(text, preferred_quote)
|
QuoteMetadataKind::triple_quoted(text, preferred_quote)
|
||||||
} else {
|
} else {
|
||||||
|
@ -528,7 +529,7 @@ impl QuoteMetadataKind {
|
||||||
|
|
||||||
/// Computes if a raw string uses the preferred quote. If it does, then it's not possible
|
/// 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.
|
/// 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 mut chars = text.chars().peekable();
|
||||||
let preferred_quote_char = preferred.as_char();
|
let preferred_quote_char = preferred.as_char();
|
||||||
|
|
||||||
|
@ -540,7 +541,7 @@ impl QuoteMetadataKind {
|
||||||
}
|
}
|
||||||
// `"` or `'`
|
// `"` or `'`
|
||||||
Some(c) if c == preferred_quote_char => {
|
Some(c) if c == preferred_quote_char => {
|
||||||
if !triple_quoted {
|
if triple_quotes.is_no() {
|
||||||
break true;
|
break true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1057,7 +1058,7 @@ mod tests {
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use ruff_python_ast::{
|
use ruff_python_ast::{
|
||||||
str::Quote,
|
str::{Quote, TripleQuotes},
|
||||||
str_prefix::{AnyStringPrefix, ByteStringPrefix},
|
str_prefix::{AnyStringPrefix, ByteStringPrefix},
|
||||||
AnyStringFlags,
|
AnyStringFlags,
|
||||||
};
|
};
|
||||||
|
@ -1086,7 +1087,7 @@ mod tests {
|
||||||
AnyStringFlags::new(
|
AnyStringFlags::new(
|
||||||
AnyStringPrefix::Bytes(ByteStringPrefix::Regular),
|
AnyStringPrefix::Bytes(ByteStringPrefix::Regular),
|
||||||
Quote::Double,
|
Quote::Double,
|
||||||
false,
|
TripleQuotes::No,
|
||||||
),
|
),
|
||||||
false,
|
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 struct EscapeLayout {
|
||||||
pub quote: Quote,
|
pub quote: Quote,
|
||||||
|
@ -60,23 +63,32 @@ impl<'a> UnicodeEscape<'a> {
|
||||||
Self::with_preferred_quote(source, Quote::Single)
|
Self::with_preferred_quote(source, Quote::Single)
|
||||||
}
|
}
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn str_repr<'r>(&'a self) -> StrRepr<'r, 'a> {
|
pub fn str_repr<'r>(&'a self, triple_quotes: TripleQuotes) -> StrRepr<'r, 'a> {
|
||||||
StrRepr(self)
|
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<'_, '_> {
|
impl StrRepr<'_, '_> {
|
||||||
pub fn write(&self, formatter: &mut impl std::fmt::Write) -> std::fmt::Result {
|
pub fn write(&self, formatter: &mut impl std::fmt::Write) -> std::fmt::Result {
|
||||||
let quote = self.0.layout().quote.as_char();
|
let flags = StringLiteralFlags::empty()
|
||||||
formatter.write_char(quote)?;
|
.with_quote_style(self.escape.layout().quote)
|
||||||
self.0.write_body(formatter)?;
|
.with_triple_quotes(self.triple_quotes);
|
||||||
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> {
|
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();
|
self.write(&mut s).unwrap();
|
||||||
Some(s)
|
Some(s)
|
||||||
}
|
}
|
||||||
|
@ -244,8 +256,11 @@ impl<'a> AsciiEscape<'a> {
|
||||||
Self::with_preferred_quote(source, Quote::Single)
|
Self::with_preferred_quote(source, Quote::Single)
|
||||||
}
|
}
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn bytes_repr<'r>(&'a self) -> BytesRepr<'r, 'a> {
|
pub fn bytes_repr<'r>(&'a self, triple_quotes: TripleQuotes) -> BytesRepr<'r, 'a> {
|
||||||
BytesRepr(self)
|
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<'_, '_> {
|
impl BytesRepr<'_, '_> {
|
||||||
pub fn write(&self, formatter: &mut impl std::fmt::Write) -> std::fmt::Result {
|
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('b')?;
|
||||||
formatter.write_char(quote)?;
|
formatter.write_str(flags.quote_str())?;
|
||||||
self.0.write_body(formatter)?;
|
self.escape.write_body(formatter)?;
|
||||||
formatter.write_char(quote)
|
formatter.write_str(flags.quote_str())?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_string(&self) -> Option<String> {
|
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();
|
self.write(&mut s).unwrap();
|
||||||
Some(s)
|
Some(s)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ use std::fmt;
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
|
|
||||||
use ruff_python_ast::name::Name;
|
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::{
|
use ruff_python_ast::str_prefix::{
|
||||||
AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix,
|
AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix,
|
||||||
};
|
};
|
||||||
|
@ -718,8 +718,12 @@ impl StringFlags for TokenFlags {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_triple_quoted(self) -> bool {
|
fn triple_quotes(self) -> TripleQuotes {
|
||||||
self.intersects(TokenFlags::TRIPLE_QUOTED_STRING)
|
if self.intersects(TokenFlags::TRIPLE_QUOTED_STRING) {
|
||||||
|
TripleQuotes::Yes
|
||||||
|
} else {
|
||||||
|
TripleQuotes::No
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prefix(self) -> AnyStringPrefix {
|
fn prefix(self) -> AnyStringPrefix {
|
||||||
|
@ -769,7 +773,7 @@ impl TokenFlags {
|
||||||
|
|
||||||
/// Converts this type to [`AnyStringFlags`], setting the equivalent flags.
|
/// Converts this type to [`AnyStringFlags`], setting the equivalent flags.
|
||||||
pub(crate) fn as_any_string_flags(self) -> AnyStringFlags {
|
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