Implement template strings (#17851)

This PR implements template strings (t-strings) in the parser and
formatter for Ruff.

Minimal changes necessary to compile were made in other parts of the code (e.g. ty, the linter, etc.). These will be covered properly in follow-up PRs.
This commit is contained in:
Dylan 2025-05-30 15:00:56 -05:00 committed by GitHub
parent ad024f9a09
commit 9bbf4987e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
261 changed files with 18023 additions and 1802 deletions

View file

@ -22,3 +22,8 @@ def my_func():
# Implicit string concatenation
"0.0.0.0" f"0.0.0.0{expr}0.0.0.0"
# t-strings - all ok
t"0.0.0.0"
"0.0.0.0" t"0.0.0.0{expr}0.0.0.0"
"0.0.0.0" f"0.0.0.0{expr}0.0.0.0" t"0.0.0.0{expr}0.0.0.0"

View file

@ -40,3 +40,7 @@ with tempfile.TemporaryDirectory(dir="/dev/shm") as d:
with TemporaryDirectory(dir="/tmp") as d:
pass
# ok (runtime error from t-string)
with open(t"/foo/bar", "w") as f:
f.write("def")

View file

@ -169,3 +169,13 @@ query60 = f"""
# https://github.com/astral-sh/ruff/issues/17967
query61 = f"SELECT * FROM table" # skip expressionless f-strings
# t-strings
query62 = t"SELECT * FROM table"
query63 = t"""
SELECT *,
foo
FROM ({user_input}) raw
"""
query64 = f"update {t"{table}"} set var = {t"{var}"}"
query65 = t"update {f"{table}"} set var = {f"{var}"}"

View file

@ -72,3 +72,5 @@ def not_warnings_dot_deprecated(
@not_warnings_dot_deprecated("Not warnings.deprecated, so this one *should* lead to PYI053 in a stub!")
def not_a_deprecated_function() -> None: ...
baz: str = t"51 character stringgggggggggggggggggggggggggggggggg"

View file

@ -80,3 +80,7 @@ x: TypeAlias = Literal["fooooooooooooooooooooooooooooooooooooooooooooooooooooooo
# Ok
y: TypeAlias = Annotated[int, "metadataaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]
ttoo: str = t"50 character stringggggggggggggggggggggggggggggggg" # OK
tbar: str = t"51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053

View file

@ -39,3 +39,27 @@ f'\'normal\' {f'nested'} normal' # Q003
f'\'normal\' {f'nested'} "double quotes"'
f'\'normal\' {f'\'nested\' {'other'} normal'} "double quotes"' # Q003
f'\'normal\' {f'\'nested\' {'other'} "double quotes"'} normal' # Q00l
# Same as above, but with t-strings
t'This is a \'string\'' # Q003
t'This is \\ a \\\'string\'' # Q003
t'"This" is a \'string\''
f"This is a 'string'"
f"\"This\" is a 'string'"
fr'This is a \'string\''
fR'This is a \'string\''
foo = (
t'This is a'
t'\'string\'' # Q003
)
t'\'foo\' {'nested'}' # Q003
t'\'foo\' {t'nested'}' # Q003
t'\'foo\' {t'\'nested\''} \'\'' # Q003
t'normal {t'nested'} normal'
t'\'normal\' {t'nested'} normal' # Q003
t'\'normal\' {t'nested'} "double quotes"'
t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003
t'\'normal\' {t'\'nested\' {'other'} "double quotes"'} normal' # Q00l

View file

@ -37,3 +37,25 @@ f"\"normal\" {f"nested"} normal" # Q003
f"\"normal\" {f"nested"} 'single quotes'"
f"\"normal\" {f"\"nested\" {"other"} normal"} 'single quotes'" # Q003
f"\"normal\" {f"\"nested\" {"other"} 'single quotes'"} normal" # Q003
# Same as above, but with t-strings
t"This is a \"string\""
t"'This' is a \"string\""
f'This is a "string"'
f'\'This\' is a "string"'
fr"This is a \"string\""
fR"This is a \"string\""
foo = (
t"This is a"
t"\"string\""
)
t"\"foo\" {"foo"}" # Q003
t"\"foo\" {t"foo"}" # Q003
t"\"foo\" {t"\"foo\""} \"\"" # Q003
t"normal {t"nested"} normal"
t"\"normal\" {t"nested"} normal" # Q003
t"\"normal\" {t"nested"} 'single quotes'"
t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003
t"\"normal\" {t"\"nested\" {"other"} 'single quotes'"} normal" # Q003

View file

@ -1,4 +1,4 @@
# Same as `W605_0.py` but using f-strings instead.
# Same as `W605_0.py` but using f-strings and t-strings instead.
#: W605:1:10
regex = f'\.png$'
@ -66,3 +66,72 @@ s = f"TOTAL: {total}\nOK: {ok}\INCOMPLETE: {incomplete}\n"
# Debug text (should trigger)
t = f"{'\InHere'=}"
#: W605:1:10
regex = t'\.png$'
#: W605:2:1
regex = t'''
\.png$
'''
#: W605:2:6
f(
t'\_'
)
#: W605:4:6
t"""
multi-line
literal
with \_ somewhere
in the middle
"""
#: W605:1:38
value = t'new line\nand invalid escape \_ here'
#: Okay
regex = fr'\.png$'
regex = t'\\.png$'
regex = fr'''
\.png$
'''
regex = fr'''
\\.png$
'''
s = t'\\'
regex = t'\w' # noqa
regex = t'''
\w
''' # noqa
regex = t'\\\_'
value = t'\{{1}}'
value = t'\{1}'
value = t'{1:\}'
value = t"{t"\{1}"}"
value = rt"{t"\{1}"}"
# Okay
value = rt'\{{1}}'
value = rt'\{1}'
value = rt'{1:\}'
value = t"{rt"\{1}"}"
# Regression tests for https://github.com/astral-sh/ruff/issues/10434
t"{{}}+-\d"
t"\n{{}}+-\d+"
t"\n{{}}<7D>+-\d+"
# See https://github.com/astral-sh/ruff/issues/11491
total = 10
ok = 7
incomplete = 3
s = t"TOTAL: {total}\nOK: {ok}\INCOMPLETE: {incomplete}\n"
# Debug text (should trigger)
t = t"{'\InHere'=}"

View file

@ -37,8 +37,8 @@ use ruff_python_ast::str::Quote;
use ruff_python_ast::visitor::{Visitor, walk_except_handler, walk_pattern};
use ruff_python_ast::{
self as ast, AnyParameterRef, ArgOrKeyword, Comprehension, ElifElseClause, ExceptHandler, Expr,
ExprContext, FStringElement, Keyword, MatchCase, ModModule, Parameter, Parameters, Pattern,
PythonVersion, Stmt, Suite, UnaryOp,
ExprContext, InterpolatedStringElement, Keyword, MatchCase, ModModule, Parameter, Parameters,
Pattern, PythonVersion, Stmt, Suite, UnaryOp,
};
use ruff_python_ast::{PySourceType, helpers, str, visitor};
use ruff_python_codegen::{Generator, Stylist};
@ -338,6 +338,7 @@ impl<'a> Checker<'a> {
ast::BytesLiteralFlags::empty().with_quote_style(self.preferred_quote())
}
// TODO(dylan) add similar method for t-strings
/// Return the default f-string flags a generated `FString` node should use, given where we are
/// in the AST.
pub(crate) fn default_fstring_flags(&self) -> ast::FStringFlags {
@ -1897,6 +1898,10 @@ impl<'a> Visitor<'a> for Checker<'a> {
self.semantic.flags |= SemanticModelFlags::F_STRING;
visitor::walk_expr(self, expr);
}
Expr::TString(_) => {
self.semantic.flags |= SemanticModelFlags::T_STRING;
visitor::walk_expr(self, expr);
}
Expr::Named(ast::ExprNamed {
target,
value,
@ -1930,6 +1935,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
}
Expr::BytesLiteral(bytes_literal) => analyze::string_like(bytes_literal.into(), self),
Expr::FString(f_string) => analyze::string_like(f_string.into(), self),
Expr::TString(t_string) => analyze::string_like(t_string.into(), self),
_ => {}
}
@ -2119,12 +2125,15 @@ impl<'a> Visitor<'a> for Checker<'a> {
}
}
fn visit_f_string_element(&mut self, f_string_element: &'a FStringElement) {
fn visit_interpolated_string_element(
&mut self,
interpolated_string_element: &'a InterpolatedStringElement,
) {
let snapshot = self.semantic.flags;
if f_string_element.is_expression() {
self.semantic.flags |= SemanticModelFlags::F_STRING_REPLACEMENT_FIELD;
if interpolated_string_element.is_interpolation() {
self.semantic.flags |= SemanticModelFlags::INTERPOLATED_STRING_REPLACEMENT_FIELD;
}
visitor::walk_f_string_element(self, f_string_element);
visitor::walk_interpolated_string_element(self, interpolated_string_element);
self.semantic.flags = snapshot;
}
}

View file

@ -63,6 +63,9 @@ pub(crate) fn hardcoded_bind_all_interfaces(checker: &Checker, string: StringLik
}
}
}
StringLike::Bytes(_) => (),
// TODO(dylan): decide whether to trigger here
StringLike::TString(_) => (),
}
}

View file

@ -101,10 +101,11 @@ pub(crate) fn hardcoded_sql_expression(checker: &Checker, expr: &Expr) {
// f"select * from table where val = {val}"
Expr::FString(f_string)
if f_string
.value
.f_strings()
.any(|fs| fs.elements.iter().any(ast::FStringElement::is_expression)) =>
if f_string.value.f_strings().any(|fs| {
fs.elements
.iter()
.any(ast::InterpolatedStringElement::is_interpolation)
}) =>
{
concatenated_f_string(f_string, checker.locator())
}
@ -175,6 +176,8 @@ fn is_explicit_concatenation(expr: &Expr) -> Option<bool> {
Expr::DictComp(_) => Some(false),
Expr::Compare(_) => Some(false),
Expr::FString(_) => Some(true),
// TODO(dylan): decide whether to trigger here
Expr::TString(_) => Some(false),
Expr::StringLiteral(_) => Some(true),
Expr::BytesLiteral(_) => Some(false),
Expr::NoneLiteral(_) => Some(false),

View file

@ -75,7 +75,10 @@ pub(crate) fn hardcoded_tmp_directory(checker: &Checker, string: StringLike) {
}
}
}
// These are not actually strings
StringLike::Bytes(_) => (),
// TODO(dylan) - verify that we should skip these
StringLike::TString(_) => (),
}
}

View file

@ -1006,9 +1006,9 @@ fn suspicious_function(
// Ex) f"foo"
Expr::FString(ast::ExprFString { value, .. }) => {
value.elements().next().and_then(|element| {
if let ast::FStringElement::Literal(ast::FStringLiteralElement {
value, ..
}) = element
if let ast::InterpolatedStringElement::Literal(
ast::InterpolatedStringLiteralElement { value, .. },
) = element
{
Some(Either::Right(value.chars()))
} else {

View file

@ -1,6 +1,5 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
snapshot_kind: text
---
S104.py:9:1: S104 Possible binding to all interfaces
|
@ -48,6 +47,8 @@ S104.py:24:1: S104 Possible binding to all interfaces
23 | # Implicit string concatenation
24 | "0.0.0.0" f"0.0.0.0{expr}0.0.0.0"
| ^^^^^^^^^ S104
25 |
26 | # t-strings - all ok
|
S104.py:24:13: S104 Possible binding to all interfaces
@ -55,6 +56,8 @@ S104.py:24:13: S104 Possible binding to all interfaces
23 | # Implicit string concatenation
24 | "0.0.0.0" f"0.0.0.0{expr}0.0.0.0"
| ^^^^^^^ S104
25 |
26 | # t-strings - all ok
|
S104.py:24:26: S104 Possible binding to all interfaces
@ -62,4 +65,6 @@ S104.py:24:26: S104 Possible binding to all interfaces
23 | # Implicit string concatenation
24 | "0.0.0.0" f"0.0.0.0{expr}0.0.0.0"
| ^^^^^^^ S104
25 |
26 | # t-strings - all ok
|

View file

@ -604,3 +604,12 @@ S608.py:164:11: S608 Possible SQL injection vector through string-based query co
169 |
170 | # https://github.com/astral-sh/ruff/issues/17967
|
S608.py:180:11: S608 Possible SQL injection vector through string-based query construction
|
178 | FROM ({user_input}) raw
179 | """
180 | query64 = f"update {t"{table}"} set var = {t"{var}"}"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S608
181 | query65 = t"update {f"{table}"} set var = {f"{var}"}"
|

View file

@ -67,6 +67,11 @@ pub(crate) fn string_or_bytes_too_long(checker: &Checker, string: StringLike) {
StringLike::String(ast::ExprStringLiteral { value, .. }) => value.chars().count(),
StringLike::Bytes(ast::ExprBytesLiteral { value, .. }) => value.len(),
StringLike::FString(node) => count_f_string_chars(node),
// TODO(dylan): decide how to count chars, especially
// if interpolations are of different type than `str`
StringLike::TString(_) => {
return;
}
};
if length <= 50 {
return;
@ -91,8 +96,10 @@ fn count_f_string_chars(f_string: &ast::ExprFString) -> usize {
.elements
.iter()
.map(|element| match element {
ast::FStringElement::Literal(string) => string.chars().count(),
ast::FStringElement::Expression(expr) => expr.range().len().to_usize(),
ast::InterpolatedStringElement::Literal(string) => string.chars().count(),
ast::InterpolatedStringElement::Interpolation(expr) => {
expr.range().len().to_usize()
}
})
.sum(),
})

View file

@ -106,19 +106,23 @@ pub(super) fn is_empty_or_null_string(expr: &Expr) -> bool {
ast::FStringPart::FString(f_string) => f_string
.elements
.iter()
.all(is_empty_or_null_fstring_element),
.all(is_empty_or_null_interpolated_string_element),
})
}
_ => false,
}
}
fn is_empty_or_null_fstring_element(element: &ast::FStringElement) -> bool {
fn is_empty_or_null_interpolated_string_element(element: &ast::InterpolatedStringElement) -> bool {
match element {
ast::FStringElement::Literal(ast::FStringLiteralElement { value, .. }) => value.is_empty(),
ast::FStringElement::Expression(ast::FStringExpressionElement { expression, .. }) => {
is_empty_or_null_string(expression)
}
ast::InterpolatedStringElement::Literal(ast::InterpolatedStringLiteralElement {
value,
..
}) => value.is_empty(),
ast::InterpolatedStringElement::Interpolation(ast::InterpolatedElement {
expression,
..
}) => is_empty_or_null_string(expression),
}
}

View file

@ -1,7 +1,7 @@
use flake8_quotes::helpers::{contains_escaped_quote, raw_contents, unescape_string};
use flake8_quotes::settings::Quote;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::visitor::{Visitor, walk_f_string};
use ruff_python_ast::visitor::{Visitor, walk_f_string, walk_t_string};
use ruff_python_ast::{self as ast, AnyStringFlags, PythonVersion, StringFlags, StringLike};
use ruff_text_size::{Ranged, TextRange, TextSize};
@ -54,7 +54,7 @@ pub(crate) fn avoidable_escaped_quote(checker: &Checker, string_like: StringLike
// This rule has support for strings nested inside another f-strings but they're checked
// via the outermost f-string. This means that we shouldn't be checking any nested string
// or f-string.
|| checker.semantic().in_f_string_replacement_field()
|| checker.semantic().in_interpolated_string_replacement_field()
{
return;
}
@ -70,6 +70,7 @@ pub(crate) fn avoidable_escaped_quote(checker: &Checker, string_like: StringLike
rule_checker.visit_bytes_literal(bytes_literal);
}
ast::StringLikePart::FString(f_string) => rule_checker.visit_f_string(f_string),
ast::StringLikePart::TString(t_string) => rule_checker.visit_t_string(t_string),
}
}
}
@ -179,25 +180,70 @@ impl Visitor<'_> for AvoidableEscapedQuoteChecker<'_, '_> {
.literals()
.any(|literal| contains_quote(literal, opposite_quote_char))
{
check_f_string(self.checker, self.quotes_settings, f_string);
check_interpolated_string(
self.checker,
self.quotes_settings,
AnyStringFlags::from(f_string.flags),
&f_string.elements,
f_string.range,
);
}
walk_f_string(self, f_string);
}
fn visit_t_string(&mut self, t_string: &'_ ast::TString) {
let opposite_quote_char = self.quotes_settings.inline_quotes.opposite().as_char();
// If any literal part of this t-string contains the quote character which is opposite to
// the configured inline quotes, we can't change the quote style for this t-string. For
// example:
//
// ```py
// t"\"hello\" {x} 'world'"
// ```
//
// If we try to fix the above example, the t-string will end in the middle and "world" will
// be considered as a variable which is outside this t-string:
//
// ```py
// t'"hello" {x} 'world''
// # ^
// # t-string ends here now
// ```
//
// The check is local to this t-string and it shouldn't check for any literal parts of any
// nested t-string.
if !t_string
.elements
.literals()
.any(|literal| contains_quote(literal, opposite_quote_char))
{
check_interpolated_string(
self.checker,
self.quotes_settings,
AnyStringFlags::from(t_string.flags),
&t_string.elements,
t_string.range,
);
}
walk_t_string(self, t_string);
}
}
/// Checks for unnecessary escaped quotes in a string or bytes literal.
///
/// # Panics
///
/// If the string kind is an f-string.
/// If the string kind is an f-string or a t-string.
fn check_string_or_bytes(
checker: &Checker,
quotes_settings: &flake8_quotes::settings::Settings,
range: TextRange,
flags: AnyStringFlags,
) {
assert!(!flags.is_f_string());
assert!(!flags.is_interpolated_string());
let locator = checker.locator();
@ -231,16 +277,14 @@ fn check_string_or_bytes(
)));
}
/// Checks for unnecessary escaped quotes in an f-string.
fn check_f_string(
/// Checks for unnecessary escaped quotes in an f-string or t-string.
fn check_interpolated_string(
checker: &Checker,
quotes_settings: &flake8_quotes::settings::Settings,
f_string: &ast::FString,
flags: ast::AnyStringFlags,
elements: &ast::InterpolatedStringElements,
range: TextRange,
) {
let locator = checker.locator();
let ast::FString { flags, range, .. } = f_string;
if flags.is_triple_quoted() || flags.prefix().is_raw() {
return;
}
@ -254,8 +298,8 @@ fn check_f_string(
let opposite_quote_char = quotes_settings.inline_quotes.opposite().as_char();
let mut edits = vec![];
for literal in f_string.elements.literals() {
let content = locator.slice(literal);
for literal in elements.literals() {
let content = checker.locator().slice(literal);
if !contains_escaped_quote(content, quote_char) {
continue;
}
@ -269,10 +313,10 @@ fn check_f_string(
return;
}
// Replacement for the f-string opening quote. We don't perform the check for raw and
// Replacement for the f/t-string opening quote. We don't perform the check for raw and
// triple-quoted f-strings, so no need to account for them.
let start_edit = Edit::range_replacement(
format!("f{opposite_quote_char}"),
format!("{}{opposite_quote_char}", flags.prefix()),
TextRange::at(
range.start(),
// Prefix + quote char
@ -280,16 +324,15 @@ fn check_f_string(
),
);
// Replacement for the f-string ending quote. We don't perform the check for triple-quoted
// Replacement for the f/t-string ending quote. We don't perform the check for triple-quoted
// f-string, so no need to account for them.
edits.push(Edit::range_replacement(
opposite_quote_char.to_string(),
TextRange::at(
// Offset would either be the end offset of the start edit in case there are no
// elements in the f-string (e.g., `f""`) or the end offset of the last f-string
// elements in the f/t-string (e.g., `f""`) or the end offset of the last f/t-string
// element (e.g., `f"hello"`).
f_string
.elements
elements
.last()
.map_or_else(|| start_edit.end(), Ranged::end),
// Quote char
@ -298,7 +341,7 @@ fn check_f_string(
));
checker
.report_diagnostic(AvoidableEscapedQuote, *range)
.report_diagnostic(AvoidableEscapedQuote, range)
.set_fix(Fix::safe_edits(start_edit, edits));
}
@ -320,6 +363,11 @@ impl Visitor<'_> for ContainsAnyString {
self.result = true;
// We don't need to recurse into this f-string now that we already know the result.
}
fn visit_t_string(&mut self, _: &'_ ast::TString) {
self.result = true;
// We don't need to recurse into this t-string now that we already know the result.
}
}
/// Return `true` if the haystack contains the quote.

View file

@ -444,7 +444,10 @@ pub(crate) fn check_string_quotes(checker: &Checker, string_like: StringLike) {
}
// TODO(dhruvmanila): Support checking for escaped quotes in f-strings.
if checker.semantic().in_f_string_replacement_field() {
if checker
.semantic()
.in_interpolated_string_replacement_field()
{
return;
}

View file

@ -1,5 +1,7 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, AnyStringFlags, StringFlags, StringLike};
use ruff_python_ast::{
self as ast, AnyStringFlags, InterpolatedStringElements, StringFlags, StringLike,
};
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
@ -62,7 +64,20 @@ pub(crate) fn unnecessary_escaped_quote(checker: &Checker, string_like: StringLi
bytes_literal.range(),
AnyStringFlags::from(bytes_literal.flags),
),
ast::StringLikePart::FString(f_string) => check_f_string(checker, f_string),
ast::StringLikePart::FString(ast::FString {
elements,
range,
flags,
}) => {
check_interpolated_string(checker, AnyStringFlags::from(*flags), *range, elements);
}
ast::StringLikePart::TString(ast::TString {
elements,
range,
flags,
}) => {
check_interpolated_string(checker, AnyStringFlags::from(*flags), *range, elements);
}
}
}
}
@ -73,7 +88,7 @@ pub(crate) fn unnecessary_escaped_quote(checker: &Checker, string_like: StringLi
///
/// If the string kind is an f-string.
fn check_string_or_bytes(checker: &Checker, range: TextRange, flags: AnyStringFlags) {
assert!(!flags.is_f_string());
assert!(!flags.is_interpolated_string());
if flags.is_triple_quoted() || flags.is_raw_string() {
return;
@ -96,9 +111,13 @@ fn check_string_or_bytes(checker: &Checker, range: TextRange, flags: AnyStringFl
)));
}
/// Checks for unnecessary escaped quotes in an f-string.
fn check_f_string(checker: &Checker, f_string: &ast::FString) {
let ast::FString { flags, range, .. } = f_string;
/// Checks for unnecessary escaped quotes in an f-string or t-string.
fn check_interpolated_string(
checker: &Checker,
flags: AnyStringFlags,
range: TextRange,
elements: &InterpolatedStringElements,
) {
if flags.is_triple_quoted() || flags.prefix().is_raw() {
return;
}
@ -106,7 +125,7 @@ fn check_f_string(checker: &Checker, f_string: &ast::FString) {
let opposite_quote_char = flags.quote_style().opposite().as_char();
let mut edits = vec![];
for literal in f_string.elements.literals() {
for literal in elements.literals() {
let content = checker.locator().slice(literal);
if !contains_escaped_quote(content, opposite_quote_char) {
continue;
@ -122,6 +141,6 @@ fn check_f_string(checker: &Checker, f_string: &ast::FString) {
return;
};
let mut diagnostic = checker.report_diagnostic(UnnecessaryEscapedQuote, *range);
let mut diagnostic = checker.report_diagnostic(UnnecessaryEscapedQuote, range);
diagnostic.set_fix(Fix::safe_edits(first, edits_iter));
}

View file

@ -197,6 +197,8 @@ singles_escaped.py:38:15: Q003 [*] Change outer quotes to avoid escaping inner q
38 |-f"\"normal\" {f"\"nested\" {"other"} normal"} 'single quotes'" # Q003
38 |+f"\"normal\" {f'"nested" {"other"} normal'} 'single quotes'" # Q003
39 39 | f"\"normal\" {f"\"nested\" {"other"} 'single quotes'"} normal" # Q003
40 40 |
41 41 |
singles_escaped.py:39:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
@ -213,3 +215,184 @@ singles_escaped.py:39:1: Q003 [*] Change outer quotes to avoid escaping inner qu
38 38 | f"\"normal\" {f"\"nested\" {"other"} normal"} 'single quotes'" # Q003
39 |-f"\"normal\" {f"\"nested\" {"other"} 'single quotes'"} normal" # Q003
39 |+f'"normal" {f"\"nested\" {"other"} 'single quotes'"} normal' # Q003
40 40 |
41 41 |
42 42 | # Same as above, but with t-strings
singles_escaped.py:43:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
42 | # Same as above, but with t-strings
43 | t"This is a \"string\""
| ^^^^^^^^^^^^^^^^^^^^^^^ Q003
44 | t"'This' is a \"string\""
45 | f'This is a "string"'
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
40 40 |
41 41 |
42 42 | # Same as above, but with t-strings
43 |-t"This is a \"string\""
43 |+t'This is a "string"'
44 44 | t"'This' is a \"string\""
45 45 | f'This is a "string"'
46 46 | f'\'This\' is a "string"'
singles_escaped.py:51:5: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
49 | foo = (
50 | t"This is a"
51 | t"\"string\""
| ^^^^^^^^^^^^^ Q003
52 | )
53 | t"\"foo\" {"foo"}" # Q003
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
48 48 | fR"This is a \"string\""
49 49 | foo = (
50 50 | t"This is a"
51 |- t"\"string\""
51 |+ t'"string"'
52 52 | )
53 53 | t"\"foo\" {"foo"}" # Q003
54 54 | t"\"foo\" {t"foo"}" # Q003
singles_escaped.py:53:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
51 | t"\"string\""
52 | )
53 | t"\"foo\" {"foo"}" # Q003
| ^^^^^^^^^^^^^^^^^^ Q003
54 | t"\"foo\" {t"foo"}" # Q003
55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
50 50 | t"This is a"
51 51 | t"\"string\""
52 52 | )
53 |-t"\"foo\" {"foo"}" # Q003
53 |+t'"foo" {"foo"}' # Q003
54 54 | t"\"foo\" {t"foo"}" # Q003
55 55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003
56 56 |
singles_escaped.py:54:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
52 | )
53 | t"\"foo\" {"foo"}" # Q003
54 | t"\"foo\" {t"foo"}" # Q003
| ^^^^^^^^^^^^^^^^^^^ Q003
55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
51 51 | t"\"string\""
52 52 | )
53 53 | t"\"foo\" {"foo"}" # Q003
54 |-t"\"foo\" {t"foo"}" # Q003
54 |+t'"foo" {t"foo"}' # Q003
55 55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003
56 56 |
57 57 | t"normal {t"nested"} normal"
singles_escaped.py:55:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
53 | t"\"foo\" {"foo"}" # Q003
54 | t"\"foo\" {t"foo"}" # Q003
55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
56 |
57 | t"normal {t"nested"} normal"
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
52 52 | )
53 53 | t"\"foo\" {"foo"}" # Q003
54 54 | t"\"foo\" {t"foo"}" # Q003
55 |-t"\"foo\" {t"\"foo\""} \"\"" # Q003
55 |+t'"foo" {t"\"foo\""} ""' # Q003
56 56 |
57 57 | t"normal {t"nested"} normal"
58 58 | t"\"normal\" {t"nested"} normal" # Q003
singles_escaped.py:55:12: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
53 | t"\"foo\" {"foo"}" # Q003
54 | t"\"foo\" {t"foo"}" # Q003
55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003
| ^^^^^^^^^^ Q003
56 |
57 | t"normal {t"nested"} normal"
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
52 52 | )
53 53 | t"\"foo\" {"foo"}" # Q003
54 54 | t"\"foo\" {t"foo"}" # Q003
55 |-t"\"foo\" {t"\"foo\""} \"\"" # Q003
55 |+t"\"foo\" {t'"foo"'} \"\"" # Q003
56 56 |
57 57 | t"normal {t"nested"} normal"
58 58 | t"\"normal\" {t"nested"} normal" # Q003
singles_escaped.py:58:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
57 | t"normal {t"nested"} normal"
58 | t"\"normal\" {t"nested"} normal" # Q003
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
59 | t"\"normal\" {t"nested"} 'single quotes'"
60 | t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
55 55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003
56 56 |
57 57 | t"normal {t"nested"} normal"
58 |-t"\"normal\" {t"nested"} normal" # Q003
58 |+t'"normal" {t"nested"} normal' # Q003
59 59 | t"\"normal\" {t"nested"} 'single quotes'"
60 60 | t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003
61 61 | t"\"normal\" {t"\"nested\" {"other"} 'single quotes'"} normal" # Q003
singles_escaped.py:60:15: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
58 | t"\"normal\" {t"nested"} normal" # Q003
59 | t"\"normal\" {t"nested"} 'single quotes'"
60 | t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
61 | t"\"normal\" {t"\"nested\" {"other"} 'single quotes'"} normal" # Q003
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
57 57 | t"normal {t"nested"} normal"
58 58 | t"\"normal\" {t"nested"} normal" # Q003
59 59 | t"\"normal\" {t"nested"} 'single quotes'"
60 |-t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003
60 |+t"\"normal\" {t'"nested" {"other"} normal'} 'single quotes'" # Q003
61 61 | t"\"normal\" {t"\"nested\" {"other"} 'single quotes'"} normal" # Q003
singles_escaped.py:61:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
59 | t"\"normal\" {t"nested"} 'single quotes'"
60 | t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003
61 | t"\"normal\" {t"\"nested\" {"other"} 'single quotes'"} normal" # Q003
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
58 58 | t"\"normal\" {t"nested"} normal" # Q003
59 59 | t"\"normal\" {t"nested"} 'single quotes'"
60 60 | t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003
61 |-t"\"normal\" {t"\"nested\" {"other"} 'single quotes'"} normal" # Q003
61 |+t'"normal" {t"\"nested\" {"other"} 'single quotes'"} normal' # Q003

View file

@ -1,6 +1,5 @@
---
source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs
snapshot_kind: text
---
singles_escaped.py:1:26: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
@ -77,3 +76,181 @@ singles_escaped.py:21:5: Q003 [*] Change outer quotes to avoid escaping inner qu
22 22 | )
23 23 |
24 24 | # Nested f-strings (Python 3.12+)
singles_escaped.py:43:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
42 | # Same as above, but with t-strings
43 | t"This is a \"string\""
| ^^^^^^^^^^^^^^^^^^^^^^^ Q003
44 | t"'This' is a \"string\""
45 | f'This is a "string"'
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
40 40 |
41 41 |
42 42 | # Same as above, but with t-strings
43 |-t"This is a \"string\""
43 |+t'This is a "string"'
44 44 | t"'This' is a \"string\""
45 45 | f'This is a "string"'
46 46 | f'\'This\' is a "string"'
singles_escaped.py:51:5: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
49 | foo = (
50 | t"This is a"
51 | t"\"string\""
| ^^^^^^^^^^^^^ Q003
52 | )
53 | t"\"foo\" {"foo"}" # Q003
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
48 48 | fR"This is a \"string\""
49 49 | foo = (
50 50 | t"This is a"
51 |- t"\"string\""
51 |+ t'"string"'
52 52 | )
53 53 | t"\"foo\" {"foo"}" # Q003
54 54 | t"\"foo\" {t"foo"}" # Q003
singles_escaped.py:53:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
51 | t"\"string\""
52 | )
53 | t"\"foo\" {"foo"}" # Q003
| ^^^^^^^^^^^^^^^^^^ Q003
54 | t"\"foo\" {t"foo"}" # Q003
55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
50 50 | t"This is a"
51 51 | t"\"string\""
52 52 | )
53 |-t"\"foo\" {"foo"}" # Q003
53 |+t'"foo" {"foo"}' # Q003
54 54 | t"\"foo\" {t"foo"}" # Q003
55 55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003
56 56 |
singles_escaped.py:54:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
52 | )
53 | t"\"foo\" {"foo"}" # Q003
54 | t"\"foo\" {t"foo"}" # Q003
| ^^^^^^^^^^^^^^^^^^^ Q003
55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
51 51 | t"\"string\""
52 52 | )
53 53 | t"\"foo\" {"foo"}" # Q003
54 |-t"\"foo\" {t"foo"}" # Q003
54 |+t'"foo" {t"foo"}' # Q003
55 55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003
56 56 |
57 57 | t"normal {t"nested"} normal"
singles_escaped.py:55:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
53 | t"\"foo\" {"foo"}" # Q003
54 | t"\"foo\" {t"foo"}" # Q003
55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
56 |
57 | t"normal {t"nested"} normal"
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
52 52 | )
53 53 | t"\"foo\" {"foo"}" # Q003
54 54 | t"\"foo\" {t"foo"}" # Q003
55 |-t"\"foo\" {t"\"foo\""} \"\"" # Q003
55 |+t'"foo" {t"\"foo\""} ""' # Q003
56 56 |
57 57 | t"normal {t"nested"} normal"
58 58 | t"\"normal\" {t"nested"} normal" # Q003
singles_escaped.py:55:12: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
53 | t"\"foo\" {"foo"}" # Q003
54 | t"\"foo\" {t"foo"}" # Q003
55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003
| ^^^^^^^^^^ Q003
56 |
57 | t"normal {t"nested"} normal"
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
52 52 | )
53 53 | t"\"foo\" {"foo"}" # Q003
54 54 | t"\"foo\" {t"foo"}" # Q003
55 |-t"\"foo\" {t"\"foo\""} \"\"" # Q003
55 |+t"\"foo\" {t'"foo"'} \"\"" # Q003
56 56 |
57 57 | t"normal {t"nested"} normal"
58 58 | t"\"normal\" {t"nested"} normal" # Q003
singles_escaped.py:58:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
57 | t"normal {t"nested"} normal"
58 | t"\"normal\" {t"nested"} normal" # Q003
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
59 | t"\"normal\" {t"nested"} 'single quotes'"
60 | t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
55 55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003
56 56 |
57 57 | t"normal {t"nested"} normal"
58 |-t"\"normal\" {t"nested"} normal" # Q003
58 |+t'"normal" {t"nested"} normal' # Q003
59 59 | t"\"normal\" {t"nested"} 'single quotes'"
60 60 | t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003
61 61 | t"\"normal\" {t"\"nested\" {"other"} 'single quotes'"} normal" # Q003
singles_escaped.py:60:15: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
58 | t"\"normal\" {t"nested"} normal" # Q003
59 | t"\"normal\" {t"nested"} 'single quotes'"
60 | t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
61 | t"\"normal\" {t"\"nested\" {"other"} 'single quotes'"} normal" # Q003
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
57 57 | t"normal {t"nested"} normal"
58 58 | t"\"normal\" {t"nested"} normal" # Q003
59 59 | t"\"normal\" {t"nested"} 'single quotes'"
60 |-t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003
60 |+t"\"normal\" {t'"nested" {"other"} normal'} 'single quotes'" # Q003
61 61 | t"\"normal\" {t"\"nested\" {"other"} 'single quotes'"} normal" # Q003
singles_escaped.py:61:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
59 | t"\"normal\" {t"nested"} 'single quotes'"
60 | t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003
61 | t"\"normal\" {t"\"nested\" {"other"} 'single quotes'"} normal" # Q003
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
58 58 | t"\"normal\" {t"nested"} normal" # Q003
59 59 | t"\"normal\" {t"nested"} 'single quotes'"
60 60 | t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003
61 |-t"\"normal\" {t"\"nested\" {"other"} 'single quotes'"} normal" # Q003
61 |+t'"normal" {t"\"nested\" {"other"} 'single quotes'"} normal' # Q003

View file

@ -236,6 +236,8 @@ doubles_escaped.py:40:15: Q003 [*] Change outer quotes to avoid escaping inner q
40 |-f'\'normal\' {f'\'nested\' {'other'} normal'} "double quotes"' # Q003
40 |+f'\'normal\' {f"'nested' {'other'} normal"} "double quotes"' # Q003
41 41 | f'\'normal\' {f'\'nested\' {'other'} "double quotes"'} normal' # Q00l
42 42 |
43 43 |
doubles_escaped.py:41:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
@ -252,3 +254,205 @@ doubles_escaped.py:41:1: Q003 [*] Change outer quotes to avoid escaping inner qu
40 40 | f'\'normal\' {f'\'nested\' {'other'} normal'} "double quotes"' # Q003
41 |-f'\'normal\' {f'\'nested\' {'other'} "double quotes"'} normal' # Q00l
41 |+f"'normal' {f'\'nested\' {'other'} "double quotes"'} normal" # Q00l
42 42 |
43 43 |
44 44 |
doubles_escaped.py:46:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
45 | # Same as above, but with t-strings
46 | t'This is a \'string\'' # Q003
| ^^^^^^^^^^^^^^^^^^^^^^^ Q003
47 | t'This is \\ a \\\'string\'' # Q003
48 | t'"This" is a \'string\''
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
43 43 |
44 44 |
45 45 | # Same as above, but with t-strings
46 |-t'This is a \'string\'' # Q003
46 |+t"This is a 'string'" # Q003
47 47 | t'This is \\ a \\\'string\'' # Q003
48 48 | t'"This" is a \'string\''
49 49 | f"This is a 'string'"
doubles_escaped.py:47:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
45 | # Same as above, but with t-strings
46 | t'This is a \'string\'' # Q003
47 | t'This is \\ a \\\'string\'' # Q003
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
48 | t'"This" is a \'string\''
49 | f"This is a 'string'"
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
44 44 |
45 45 | # Same as above, but with t-strings
46 46 | t'This is a \'string\'' # Q003
47 |-t'This is \\ a \\\'string\'' # Q003
47 |+t"This is \\ a \\'string'" # Q003
48 48 | t'"This" is a \'string\''
49 49 | f"This is a 'string'"
50 50 | f"\"This\" is a 'string'"
doubles_escaped.py:55:5: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
53 | foo = (
54 | t'This is a'
55 | t'\'string\'' # Q003
| ^^^^^^^^^^^^^ Q003
56 | )
57 | t'\'foo\' {'nested'}' # Q003
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
52 52 | fR'This is a \'string\''
53 53 | foo = (
54 54 | t'This is a'
55 |- t'\'string\'' # Q003
55 |+ t"'string'" # Q003
56 56 | )
57 57 | t'\'foo\' {'nested'}' # Q003
58 58 | t'\'foo\' {t'nested'}' # Q003
doubles_escaped.py:57:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
55 | t'\'string\'' # Q003
56 | )
57 | t'\'foo\' {'nested'}' # Q003
| ^^^^^^^^^^^^^^^^^^^^^ Q003
58 | t'\'foo\' {t'nested'}' # Q003
59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
54 54 | t'This is a'
55 55 | t'\'string\'' # Q003
56 56 | )
57 |-t'\'foo\' {'nested'}' # Q003
57 |+t"'foo' {'nested'}" # Q003
58 58 | t'\'foo\' {t'nested'}' # Q003
59 59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003
60 60 |
doubles_escaped.py:58:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
56 | )
57 | t'\'foo\' {'nested'}' # Q003
58 | t'\'foo\' {t'nested'}' # Q003
| ^^^^^^^^^^^^^^^^^^^^^^ Q003
59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
55 55 | t'\'string\'' # Q003
56 56 | )
57 57 | t'\'foo\' {'nested'}' # Q003
58 |-t'\'foo\' {t'nested'}' # Q003
58 |+t"'foo' {t'nested'}" # Q003
59 59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003
60 60 |
61 61 | t'normal {t'nested'} normal'
doubles_escaped.py:59:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
57 | t'\'foo\' {'nested'}' # Q003
58 | t'\'foo\' {t'nested'}' # Q003
59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
60 |
61 | t'normal {t'nested'} normal'
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
56 56 | )
57 57 | t'\'foo\' {'nested'}' # Q003
58 58 | t'\'foo\' {t'nested'}' # Q003
59 |-t'\'foo\' {t'\'nested\''} \'\'' # Q003
59 |+t"'foo' {t'\'nested\''} ''" # Q003
60 60 |
61 61 | t'normal {t'nested'} normal'
62 62 | t'\'normal\' {t'nested'} normal' # Q003
doubles_escaped.py:59:12: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
57 | t'\'foo\' {'nested'}' # Q003
58 | t'\'foo\' {t'nested'}' # Q003
59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003
| ^^^^^^^^^^^^^ Q003
60 |
61 | t'normal {t'nested'} normal'
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
56 56 | )
57 57 | t'\'foo\' {'nested'}' # Q003
58 58 | t'\'foo\' {t'nested'}' # Q003
59 |-t'\'foo\' {t'\'nested\''} \'\'' # Q003
59 |+t'\'foo\' {t"'nested'"} \'\'' # Q003
60 60 |
61 61 | t'normal {t'nested'} normal'
62 62 | t'\'normal\' {t'nested'} normal' # Q003
doubles_escaped.py:62:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
61 | t'normal {t'nested'} normal'
62 | t'\'normal\' {t'nested'} normal' # Q003
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
63 | t'\'normal\' {t'nested'} "double quotes"'
64 | t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
59 59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003
60 60 |
61 61 | t'normal {t'nested'} normal'
62 |-t'\'normal\' {t'nested'} normal' # Q003
62 |+t"'normal' {t'nested'} normal" # Q003
63 63 | t'\'normal\' {t'nested'} "double quotes"'
64 64 | t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003
65 65 | t'\'normal\' {t'\'nested\' {'other'} "double quotes"'} normal' # Q00l
doubles_escaped.py:64:15: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
62 | t'\'normal\' {t'nested'} normal' # Q003
63 | t'\'normal\' {t'nested'} "double quotes"'
64 | t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
65 | t'\'normal\' {t'\'nested\' {'other'} "double quotes"'} normal' # Q00l
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
61 61 | t'normal {t'nested'} normal'
62 62 | t'\'normal\' {t'nested'} normal' # Q003
63 63 | t'\'normal\' {t'nested'} "double quotes"'
64 |-t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003
64 |+t'\'normal\' {t"'nested' {'other'} normal"} "double quotes"' # Q003
65 65 | t'\'normal\' {t'\'nested\' {'other'} "double quotes"'} normal' # Q00l
doubles_escaped.py:65:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
63 | t'\'normal\' {t'nested'} "double quotes"'
64 | t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003
65 | t'\'normal\' {t'\'nested\' {'other'} "double quotes"'} normal' # Q00l
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
62 62 | t'\'normal\' {t'nested'} normal' # Q003
63 63 | t'\'normal\' {t'nested'} "double quotes"'
64 64 | t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003
65 |-t'\'normal\' {t'\'nested\' {'other'} "double quotes"'} normal' # Q00l
65 |+t"'normal' {t'\'nested\' {'other'} "double quotes"'} normal" # Q00l

View file

@ -1,6 +1,5 @@
---
source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs
snapshot_kind: text
---
doubles_escaped.py:1:26: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
@ -116,3 +115,202 @@ doubles_escaped.py:23:5: Q003 [*] Change outer quotes to avoid escaping inner qu
24 24 | )
25 25 |
26 26 | # Nested f-strings (Python 3.12+)
doubles_escaped.py:46:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
45 | # Same as above, but with t-strings
46 | t'This is a \'string\'' # Q003
| ^^^^^^^^^^^^^^^^^^^^^^^ Q003
47 | t'This is \\ a \\\'string\'' # Q003
48 | t'"This" is a \'string\''
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
43 43 |
44 44 |
45 45 | # Same as above, but with t-strings
46 |-t'This is a \'string\'' # Q003
46 |+t"This is a 'string'" # Q003
47 47 | t'This is \\ a \\\'string\'' # Q003
48 48 | t'"This" is a \'string\''
49 49 | f"This is a 'string'"
doubles_escaped.py:47:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
45 | # Same as above, but with t-strings
46 | t'This is a \'string\'' # Q003
47 | t'This is \\ a \\\'string\'' # Q003
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
48 | t'"This" is a \'string\''
49 | f"This is a 'string'"
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
44 44 |
45 45 | # Same as above, but with t-strings
46 46 | t'This is a \'string\'' # Q003
47 |-t'This is \\ a \\\'string\'' # Q003
47 |+t"This is \\ a \\'string'" # Q003
48 48 | t'"This" is a \'string\''
49 49 | f"This is a 'string'"
50 50 | f"\"This\" is a 'string'"
doubles_escaped.py:55:5: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
53 | foo = (
54 | t'This is a'
55 | t'\'string\'' # Q003
| ^^^^^^^^^^^^^ Q003
56 | )
57 | t'\'foo\' {'nested'}' # Q003
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
52 52 | fR'This is a \'string\''
53 53 | foo = (
54 54 | t'This is a'
55 |- t'\'string\'' # Q003
55 |+ t"'string'" # Q003
56 56 | )
57 57 | t'\'foo\' {'nested'}' # Q003
58 58 | t'\'foo\' {t'nested'}' # Q003
doubles_escaped.py:57:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
55 | t'\'string\'' # Q003
56 | )
57 | t'\'foo\' {'nested'}' # Q003
| ^^^^^^^^^^^^^^^^^^^^^ Q003
58 | t'\'foo\' {t'nested'}' # Q003
59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
54 54 | t'This is a'
55 55 | t'\'string\'' # Q003
56 56 | )
57 |-t'\'foo\' {'nested'}' # Q003
57 |+t"'foo' {'nested'}" # Q003
58 58 | t'\'foo\' {t'nested'}' # Q003
59 59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003
60 60 |
doubles_escaped.py:58:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
56 | )
57 | t'\'foo\' {'nested'}' # Q003
58 | t'\'foo\' {t'nested'}' # Q003
| ^^^^^^^^^^^^^^^^^^^^^^ Q003
59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
55 55 | t'\'string\'' # Q003
56 56 | )
57 57 | t'\'foo\' {'nested'}' # Q003
58 |-t'\'foo\' {t'nested'}' # Q003
58 |+t"'foo' {t'nested'}" # Q003
59 59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003
60 60 |
61 61 | t'normal {t'nested'} normal'
doubles_escaped.py:59:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
57 | t'\'foo\' {'nested'}' # Q003
58 | t'\'foo\' {t'nested'}' # Q003
59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
60 |
61 | t'normal {t'nested'} normal'
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
56 56 | )
57 57 | t'\'foo\' {'nested'}' # Q003
58 58 | t'\'foo\' {t'nested'}' # Q003
59 |-t'\'foo\' {t'\'nested\''} \'\'' # Q003
59 |+t"'foo' {t'\'nested\''} ''" # Q003
60 60 |
61 61 | t'normal {t'nested'} normal'
62 62 | t'\'normal\' {t'nested'} normal' # Q003
doubles_escaped.py:59:12: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
57 | t'\'foo\' {'nested'}' # Q003
58 | t'\'foo\' {t'nested'}' # Q003
59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003
| ^^^^^^^^^^^^^ Q003
60 |
61 | t'normal {t'nested'} normal'
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
56 56 | )
57 57 | t'\'foo\' {'nested'}' # Q003
58 58 | t'\'foo\' {t'nested'}' # Q003
59 |-t'\'foo\' {t'\'nested\''} \'\'' # Q003
59 |+t'\'foo\' {t"'nested'"} \'\'' # Q003
60 60 |
61 61 | t'normal {t'nested'} normal'
62 62 | t'\'normal\' {t'nested'} normal' # Q003
doubles_escaped.py:62:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
61 | t'normal {t'nested'} normal'
62 | t'\'normal\' {t'nested'} normal' # Q003
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
63 | t'\'normal\' {t'nested'} "double quotes"'
64 | t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
59 59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003
60 60 |
61 61 | t'normal {t'nested'} normal'
62 |-t'\'normal\' {t'nested'} normal' # Q003
62 |+t"'normal' {t'nested'} normal" # Q003
63 63 | t'\'normal\' {t'nested'} "double quotes"'
64 64 | t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003
65 65 | t'\'normal\' {t'\'nested\' {'other'} "double quotes"'} normal' # Q00l
doubles_escaped.py:64:15: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
62 | t'\'normal\' {t'nested'} normal' # Q003
63 | t'\'normal\' {t'nested'} "double quotes"'
64 | t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
65 | t'\'normal\' {t'\'nested\' {'other'} "double quotes"'} normal' # Q00l
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
61 61 | t'normal {t'nested'} normal'
62 62 | t'\'normal\' {t'nested'} normal' # Q003
63 63 | t'\'normal\' {t'nested'} "double quotes"'
64 |-t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003
64 |+t'\'normal\' {t"'nested' {'other'} normal"} "double quotes"' # Q003
65 65 | t'\'normal\' {t'\'nested\' {'other'} "double quotes"'} normal' # Q00l
doubles_escaped.py:65:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
63 | t'\'normal\' {t'nested'} "double quotes"'
64 | t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003
65 | t'\'normal\' {t'\'nested\' {'other'} "double quotes"'} normal' # Q00l
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
|
= help: Change outer quotes to avoid escaping inner quotes
Safe fix
62 62 | t'\'normal\' {t'nested'} normal' # Q003
63 63 | t'\'normal\' {t'nested'} "double quotes"'
64 64 | t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003
65 |-t'\'normal\' {t'\'nested\' {'other'} "double quotes"'} normal' # Q00l
65 |+t"'normal' {t'\'nested\' {'other'} "double quotes"'} normal" # Q00l

View file

@ -2,8 +2,8 @@ use ruff_python_ast::{self as ast, Arguments, ConversionFlag, Expr};
use ruff_text_size::TextRange;
/// Wrap an expression in a [`ast::FStringElement::Expression`] with no special formatting.
fn to_f_string_expression_element(inner: &Expr) -> ast::FStringElement {
ast::FStringElement::Expression(ast::FStringExpressionElement {
fn to_interpolated_string_interpolation_element(inner: &Expr) -> ast::InterpolatedStringElement {
ast::InterpolatedStringElement::Interpolation(ast::InterpolatedElement {
expression: Box::new(inner.clone()),
debug_text: None,
conversion: ConversionFlag::None,
@ -12,9 +12,9 @@ fn to_f_string_expression_element(inner: &Expr) -> ast::FStringElement {
})
}
/// Convert a string to a [`ast::FStringElement::Literal`].
pub(super) fn to_f_string_literal_element(s: &str) -> ast::FStringElement {
ast::FStringElement::Literal(ast::FStringLiteralElement {
/// Convert a string to a [`ast::InterpolatedStringLiteralElement `].
pub(super) fn to_interpolated_string_literal_element(s: &str) -> ast::InterpolatedStringElement {
ast::InterpolatedStringElement::Literal(ast::InterpolatedStringLiteralElement {
value: Box::from(s),
range: TextRange::default(),
})
@ -48,20 +48,24 @@ fn is_simple_callee(func: &Expr) -> bool {
}
}
/// Convert an expression to a f-string element (if it looks like a good idea).
pub(super) fn to_f_string_element(expr: &Expr) -> Option<ast::FStringElement> {
/// Convert an expression to an f-string or t-string element (if it looks like a good idea).
pub(super) fn to_interpolated_string_element(
expr: &Expr,
) -> Option<ast::InterpolatedStringElement> {
match expr {
Expr::StringLiteral(ast::ExprStringLiteral { value, range }) => {
Some(ast::FStringElement::Literal(ast::FStringLiteralElement {
Expr::StringLiteral(ast::ExprStringLiteral { value, range }) => Some(
ast::InterpolatedStringElement::Literal(ast::InterpolatedStringLiteralElement {
value: value.to_string().into_boxed_str(),
range: *range,
}))
}
}),
),
// These should be pretty safe to wrap in a formatted value.
Expr::NumberLiteral(_) | Expr::BooleanLiteral(_) | Expr::Name(_) | Expr::Attribute(_) => {
Some(to_f_string_expression_element(expr))
Some(to_interpolated_string_interpolation_element(expr))
}
Expr::Call(_) if is_simple_call(expr) => {
Some(to_interpolated_string_interpolation_element(expr))
}
Expr::Call(_) if is_simple_call(expr) => Some(to_f_string_expression_element(expr)),
_ => None,
}
}

View file

@ -105,9 +105,9 @@ fn build_fstring(joiner: &str, joinees: &[Expr], flags: FStringFlags) -> Option<
return None;
}
if !std::mem::take(&mut first) {
f_string_elements.push(helpers::to_f_string_literal_element(joiner));
f_string_elements.push(helpers::to_interpolated_string_literal_element(joiner));
}
f_string_elements.push(helpers::to_f_string_element(expr)?);
f_string_elements.push(helpers::to_interpolated_string_element(expr)?);
}
let node = ast::FString {

View file

@ -1,7 +1,10 @@
use memchr::memchr_iter;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{AnyStringFlags, FStringElement, StringLike, StringLikePart};
use ruff_python_ast::{
AnyStringFlags, InterpolatedStringElement, InterpolatedStringElements, StringLike,
StringLikePart,
};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::Locator;
@ -70,39 +73,16 @@ pub(crate) fn invalid_escape_sequence(checker: &Checker, string_like: StringLike
StringLikePart::String(_) | StringLikePart::Bytes(_) => {
analyze_escape_chars(locator, part.range(), part.flags())
}
StringLikePart::FString(f_string) => {
let flags = AnyStringFlags::from(f_string.flags);
let mut escape_chars_state = EscapeCharsState::default();
// Whether we suggest converting to a raw string or
// adding backslashes depends on the presence of valid
// escape characters in the entire f-string. Therefore,
// we must analyze escape characters in each f-string
// element before pushing a diagnostic and fix.
for element in &f_string.elements {
match element {
FStringElement::Literal(literal) => {
escape_chars_state.update(analyze_escape_chars(
locator,
literal.range(),
flags,
));
}
FStringElement::Expression(expression) => {
let Some(format_spec) = expression.format_spec.as_ref() else {
continue;
};
for literal in format_spec.elements.literals() {
escape_chars_state.update(analyze_escape_chars(
locator,
literal.range(),
flags,
));
}
}
}
}
escape_chars_state
}
StringLikePart::FString(f_string) => analyze_escape_chars_in_interpolated_string(
AnyStringFlags::from(f_string.flags),
&f_string.elements,
locator,
),
StringLikePart::TString(t_string) => analyze_escape_chars_in_interpolated_string(
AnyStringFlags::from(t_string.flags),
&t_string.elements,
locator,
),
};
check(checker, locator, part.start(), part.flags(), state);
}
@ -146,7 +126,7 @@ fn analyze_escape_chars(
let next_char = match source[i + 1..].chars().next() {
Some(next_char) => next_char,
None if flags.is_f_string() => {
None if flags.is_interpolated_string() => {
// If we're at the end of a f-string middle token, the next character
// is actually emitted as a different token. For example,
//
@ -230,6 +210,39 @@ fn analyze_escape_chars(
}
}
fn analyze_escape_chars_in_interpolated_string(
flags: AnyStringFlags,
elements: &InterpolatedStringElements,
locator: &Locator,
) -> EscapeCharsState {
let mut escape_chars_state = EscapeCharsState::default();
// Whether we suggest converting to a raw string or
// adding backslashes depends on the presence of valid
// escape characters in the entire f/t-string. Therefore,
// we must analyze escape characters in each f/t-string
// element before pushing a diagnostic and fix.
for element in elements {
match element {
InterpolatedStringElement::Literal(literal) => {
escape_chars_state.update(analyze_escape_chars(locator, literal.range(), flags));
}
InterpolatedStringElement::Interpolation(interpolation) => {
let Some(format_spec) = interpolation.format_spec.as_ref() else {
continue;
};
for literal in format_spec.elements.literals() {
escape_chars_state.update(analyze_escape_chars(
locator,
literal.range(),
flags,
));
}
}
}
}
escape_chars_state
}
/// Pushes a diagnostic and fix depending on escape characters seen so far.
///
/// If we have not seen any valid escape characters, we convert to

View file

@ -12,7 +12,7 @@ W605_1.py:4:11: W605 [*] Invalid escape sequence: `\.`
= help: Use a raw string literal
Safe fix
1 1 | # Same as `W605_0.py` but using f-strings instead.
1 1 | # Same as `W605_0.py` but using f-strings and t-strings instead.
2 2 |
3 3 | #: W605:1:10
4 |-regex = f'\.png$'
@ -320,3 +320,346 @@ W605_1.py:68:9: W605 [*] Invalid escape sequence: `\I`
67 67 | # Debug text (should trigger)
68 |-t = f"{'\InHere'=}"
68 |+t = f"{r'\InHere'=}"
69 69 |
70 70 |
71 71 |
W605_1.py:73:11: W605 [*] Invalid escape sequence: `\.`
|
72 | #: W605:1:10
73 | regex = t'\.png$'
| ^^ W605
74 |
75 | #: W605:2:1
|
= help: Use a raw string literal
Safe fix
70 70 |
71 71 |
72 72 | #: W605:1:10
73 |-regex = t'\.png$'
73 |+regex = rt'\.png$'
74 74 |
75 75 | #: W605:2:1
76 76 | regex = t'''
W605_1.py:77:1: W605 [*] Invalid escape sequence: `\.`
|
75 | #: W605:2:1
76 | regex = t'''
77 | \.png$
| ^^ W605
78 | '''
|
= help: Use a raw string literal
Safe fix
73 73 | regex = t'\.png$'
74 74 |
75 75 | #: W605:2:1
76 |-regex = t'''
76 |+regex = rt'''
77 77 | \.png$
78 78 | '''
79 79 |
W605_1.py:82:7: W605 [*] Invalid escape sequence: `\_`
|
80 | #: W605:2:6
81 | f(
82 | t'\_'
| ^^ W605
83 | )
|
= help: Use a raw string literal
Safe fix
79 79 |
80 80 | #: W605:2:6
81 81 | f(
82 |- t'\_'
82 |+ rt'\_'
83 83 | )
84 84 |
85 85 | #: W605:4:6
W605_1.py:89:6: W605 [*] Invalid escape sequence: `\_`
|
87 | multi-line
88 | literal
89 | with \_ somewhere
| ^^ W605
90 | in the middle
91 | """
|
= help: Use a raw string literal
Safe fix
83 83 | )
84 84 |
85 85 | #: W605:4:6
86 |-t"""
86 |+rt"""
87 87 | multi-line
88 88 | literal
89 89 | with \_ somewhere
W605_1.py:94:40: W605 [*] Invalid escape sequence: `\_`
|
93 | #: W605:1:38
94 | value = t'new line\nand invalid escape \_ here'
| ^^ W605
|
= help: Add backslash to escape sequence
Safe fix
91 91 | """
92 92 |
93 93 | #: W605:1:38
94 |-value = t'new line\nand invalid escape \_ here'
94 |+value = t'new line\nand invalid escape \\_ here'
95 95 |
96 96 |
97 97 | #: Okay
W605_1.py:109:1: W605 [*] Invalid escape sequence: `\w`
|
107 | regex = t'\w' # noqa
108 | regex = t'''
109 | \w
| ^^ W605
110 | ''' # noqa
|
= help: Use a raw string literal
Safe fix
105 105 | '''
106 106 | s = t'\\'
107 107 | regex = t'\w' # noqa
108 |-regex = t'''
108 |+regex = rt'''
109 109 | \w
110 110 | ''' # noqa
111 111 |
W605_1.py:112:13: W605 [*] Invalid escape sequence: `\_`
|
110 | ''' # noqa
111 |
112 | regex = t'\\\_'
| ^^ W605
113 | value = t'\{{1}}'
114 | value = t'\{1}'
|
= help: Add backslash to escape sequence
Safe fix
109 109 | \w
110 110 | ''' # noqa
111 111 |
112 |-regex = t'\\\_'
112 |+regex = t'\\\\_'
113 113 | value = t'\{{1}}'
114 114 | value = t'\{1}'
115 115 | value = t'{1:\}'
W605_1.py:113:11: W605 [*] Invalid escape sequence: `\{`
|
112 | regex = t'\\\_'
113 | value = t'\{{1}}'
| ^^ W605
114 | value = t'\{1}'
115 | value = t'{1:\}'
|
= help: Use a raw string literal
Safe fix
110 110 | ''' # noqa
111 111 |
112 112 | regex = t'\\\_'
113 |-value = t'\{{1}}'
113 |+value = rt'\{{1}}'
114 114 | value = t'\{1}'
115 115 | value = t'{1:\}'
116 116 | value = t"{t"\{1}"}"
W605_1.py:114:11: W605 [*] Invalid escape sequence: `\{`
|
112 | regex = t'\\\_'
113 | value = t'\{{1}}'
114 | value = t'\{1}'
| ^^ W605
115 | value = t'{1:\}'
116 | value = t"{t"\{1}"}"
|
= help: Use a raw string literal
Safe fix
111 111 |
112 112 | regex = t'\\\_'
113 113 | value = t'\{{1}}'
114 |-value = t'\{1}'
114 |+value = rt'\{1}'
115 115 | value = t'{1:\}'
116 116 | value = t"{t"\{1}"}"
117 117 | value = rt"{t"\{1}"}"
W605_1.py:115:14: W605 [*] Invalid escape sequence: `\}`
|
113 | value = t'\{{1}}'
114 | value = t'\{1}'
115 | value = t'{1:\}'
| ^^ W605
116 | value = t"{t"\{1}"}"
117 | value = rt"{t"\{1}"}"
|
= help: Use a raw string literal
Safe fix
112 112 | regex = t'\\\_'
113 113 | value = t'\{{1}}'
114 114 | value = t'\{1}'
115 |-value = t'{1:\}'
115 |+value = rt'{1:\}'
116 116 | value = t"{t"\{1}"}"
117 117 | value = rt"{t"\{1}"}"
118 118 |
W605_1.py:116:14: W605 [*] Invalid escape sequence: `\{`
|
114 | value = t'\{1}'
115 | value = t'{1:\}'
116 | value = t"{t"\{1}"}"
| ^^ W605
117 | value = rt"{t"\{1}"}"
|
= help: Use a raw string literal
Safe fix
113 113 | value = t'\{{1}}'
114 114 | value = t'\{1}'
115 115 | value = t'{1:\}'
116 |-value = t"{t"\{1}"}"
116 |+value = t"{rt"\{1}"}"
117 117 | value = rt"{t"\{1}"}"
118 118 |
119 119 | # Okay
W605_1.py:117:15: W605 [*] Invalid escape sequence: `\{`
|
115 | value = t'{1:\}'
116 | value = t"{t"\{1}"}"
117 | value = rt"{t"\{1}"}"
| ^^ W605
118 |
119 | # Okay
|
= help: Use a raw string literal
Safe fix
114 114 | value = t'\{1}'
115 115 | value = t'{1:\}'
116 116 | value = t"{t"\{1}"}"
117 |-value = rt"{t"\{1}"}"
117 |+value = rt"{rt"\{1}"}"
118 118 |
119 119 | # Okay
120 120 | value = rt'\{{1}}'
W605_1.py:126:9: W605 [*] Invalid escape sequence: `\d`
|
125 | # Regression tests for https://github.com/astral-sh/ruff/issues/10434
126 | t"{{}}+-\d"
| ^^ W605
127 | t"\n{{}}+-\d+"
128 | t"\n{{}}<7D>+-\d+"
|
= help: Use a raw string literal
Safe fix
123 123 | value = t"{rt"\{1}"}"
124 124 |
125 125 | # Regression tests for https://github.com/astral-sh/ruff/issues/10434
126 |-t"{{}}+-\d"
126 |+rt"{{}}+-\d"
127 127 | t"\n{{}}+-\d+"
128 128 | t"\n{{}}<7D>+-\d+"
129 129 |
W605_1.py:127:11: W605 [*] Invalid escape sequence: `\d`
|
125 | # Regression tests for https://github.com/astral-sh/ruff/issues/10434
126 | t"{{}}+-\d"
127 | t"\n{{}}+-\d+"
| ^^ W605
128 | t"\n{{}}<7D>+-\d+"
|
= help: Add backslash to escape sequence
Safe fix
124 124 |
125 125 | # Regression tests for https://github.com/astral-sh/ruff/issues/10434
126 126 | t"{{}}+-\d"
127 |-t"\n{{}}+-\d+"
127 |+t"\n{{}}+-\\d+"
128 128 | t"\n{{}}<7D>+-\d+"
129 129 |
130 130 | # See https://github.com/astral-sh/ruff/issues/11491
W605_1.py:128:12: W605 [*] Invalid escape sequence: `\d`
|
126 | t"{{}}+-\d"
127 | t"\n{{}}+-\d+"
128 | t"\n{{}}<7D>+-\d+"
| ^^ W605
129 |
130 | # See https://github.com/astral-sh/ruff/issues/11491
|
= help: Add backslash to escape sequence
Safe fix
125 125 | # Regression tests for https://github.com/astral-sh/ruff/issues/10434
126 126 | t"{{}}+-\d"
127 127 | t"\n{{}}+-\d+"
128 |-t"\n{{}}<7D>+-\d+"
128 |+t"\n{{}}<7D>+-\\d+"
129 129 |
130 130 | # See https://github.com/astral-sh/ruff/issues/11491
131 131 | total = 10
W605_1.py:134:31: W605 [*] Invalid escape sequence: `\I`
|
132 | ok = 7
133 | incomplete = 3
134 | s = t"TOTAL: {total}\nOK: {ok}\INCOMPLETE: {incomplete}\n"
| ^^ W605
135 |
136 | # Debug text (should trigger)
|
= help: Add backslash to escape sequence
Safe fix
131 131 | total = 10
132 132 | ok = 7
133 133 | incomplete = 3
134 |-s = t"TOTAL: {total}\nOK: {ok}\INCOMPLETE: {incomplete}\n"
134 |+s = t"TOTAL: {total}\nOK: {ok}\\INCOMPLETE: {incomplete}\n"
135 135 |
136 136 | # Debug text (should trigger)
137 137 | t = t"{'\InHere'=}"
W605_1.py:137:9: W605 [*] Invalid escape sequence: `\I`
|
136 | # Debug text (should trigger)
137 | t = t"{'\InHere'=}"
| ^^ W605
|
= help: Use a raw string literal
Safe fix
134 134 | s = t"TOTAL: {total}\nOK: {ok}\INCOMPLETE: {incomplete}\n"
135 135 |
136 136 | # Debug text (should trigger)
137 |-t = t"{'\InHere'=}"
137 |+t = t"{r'\InHere'=}"

View file

@ -73,7 +73,7 @@ pub(crate) fn f_string_missing_placeholders(checker: &Checker, expr: &ast::ExprF
f_string
.elements
.iter()
.any(ast::FStringElement::is_expression)
.any(ast::InterpolatedStringElement::is_interpolation)
}) {
return;
}

View file

@ -77,10 +77,10 @@ pub(crate) fn assert_on_string_literal(checker: &Checker, test: &Expr) {
ast::FStringPart::Literal(literal) => literal.is_empty(),
ast::FStringPart::FString(f_string) => {
f_string.elements.iter().all(|element| match element {
ast::FStringElement::Literal(ast::FStringLiteralElement {
value, ..
}) => value.is_empty(),
ast::FStringElement::Expression(_) => false,
ast::InterpolatedStringElement::Literal(
ast::InterpolatedStringLiteralElement { value, .. },
) => value.is_empty(),
ast::InterpolatedStringElement::Interpolation(_) => false,
})
}
}) {
@ -89,10 +89,10 @@ pub(crate) fn assert_on_string_literal(checker: &Checker, test: &Expr) {
ast::FStringPart::Literal(literal) => !literal.is_empty(),
ast::FStringPart::FString(f_string) => {
f_string.elements.iter().any(|element| match element {
ast::FStringElement::Literal(ast::FStringLiteralElement {
value, ..
}) => !value.is_empty(),
ast::FStringElement::Expression(_) => false,
ast::InterpolatedStringElement::Literal(
ast::InterpolatedStringLiteralElement { value, .. },
) => !value.is_empty(),
ast::InterpolatedStringElement::Interpolation(_) => false,
})
}
}) {

View file

@ -252,6 +252,7 @@ fn is_allowed_value(expr: &Expr) -> bool {
| Expr::Compare(_)
| Expr::Call(_)
| Expr::FString(_)
| Expr::TString(_)
| Expr::StringLiteral(_)
| Expr::BytesLiteral(_)
| Expr::NumberLiteral(_)

View file

@ -137,6 +137,7 @@ pub(crate) fn bit_count(checker: &Checker, call: &ExprCall) {
Expr::StringLiteral(inner) => inner.value.is_implicit_concatenated(),
Expr::BytesLiteral(inner) => inner.value.is_implicit_concatenated(),
Expr::FString(inner) => inner.value.is_implicit_concatenated(),
Expr::TString(inner) => inner.value.is_implicit_concatenated(),
Expr::Await(_)
| Expr::Starred(_)

View file

@ -3,7 +3,7 @@ use std::fmt;
use bitflags::bitflags;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, StringLike};
use ruff_python_ast::{self as ast, FString, StringLike, TString};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::Locator;
@ -211,8 +211,9 @@ pub(crate) fn ambiguous_unicode_character_string(checker: &Checker, string_like:
}
}
ast::StringLikePart::Bytes(_) => {}
ast::StringLikePart::FString(f_string) => {
for literal in f_string.elements.literals() {
ast::StringLikePart::FString(FString { elements, .. })
| ast::StringLikePart::TString(TString { elements, .. }) => {
for literal in elements.literals() {
let text = checker.locator().slice(literal);
for candidate in
ambiguous_unicode_character(text, literal.range(), checker.settings)

View file

@ -88,9 +88,9 @@ pub(crate) fn assert_with_print_message(checker: &Checker, stmt: &ast::StmtAsser
mod print_arguments {
use itertools::Itertools;
use ruff_python_ast::{
Arguments, ConversionFlag, Expr, ExprFString, FString, FStringElement, FStringElements,
FStringExpressionElement, FStringFlags, FStringLiteralElement, FStringValue, StringLiteral,
StringLiteralFlags,
Arguments, ConversionFlag, Expr, ExprFString, FString, FStringFlags, FStringValue,
InterpolatedElement, InterpolatedStringElement, InterpolatedStringElements,
InterpolatedStringLiteralElement, StringLiteral, StringLiteralFlags,
};
use ruff_text_size::TextRange;
@ -103,14 +103,14 @@ mod print_arguments {
/// `FStringLiteralElement`.
/// - if the expression is an f-string, the elements will be returned as-is.
/// - otherwise, the expression will be wrapped in a `FStringExpressionElement`.
fn expr_to_fstring_elements(expr: &Expr) -> Vec<FStringElement> {
fn expr_to_fstring_elements(expr: &Expr) -> Vec<InterpolatedStringElement> {
match expr {
// If the expression is a string literal, convert each part to a `FStringLiteralElement`.
Expr::StringLiteral(string) => string
.value
.iter()
.map(|part| {
FStringElement::Literal(FStringLiteralElement {
InterpolatedStringElement::Literal(InterpolatedStringLiteralElement {
value: part.value.clone(),
range: TextRange::default(),
})
@ -122,13 +122,15 @@ mod print_arguments {
// Otherwise, return the expression as a single `FStringExpressionElement` wrapping
// the expression.
expr => vec![FStringElement::Expression(FStringExpressionElement {
expression: Box::new(expr.clone()),
debug_text: None,
conversion: ConversionFlag::None,
format_spec: None,
range: TextRange::default(),
})],
expr => vec![InterpolatedStringElement::Interpolation(
InterpolatedElement {
expression: Box::new(expr.clone()),
debug_text: None,
conversion: ConversionFlag::None,
format_spec: None,
range: TextRange::default(),
},
)],
}
}
@ -140,11 +142,11 @@ mod print_arguments {
/// checking if the `sep` and `args` arguments to `print` are all string
/// literals.
fn fstring_elements_to_string_literals<'a>(
mut elements: impl ExactSizeIterator<Item = &'a FStringElement>,
mut elements: impl ExactSizeIterator<Item = &'a InterpolatedStringElement>,
flags: StringLiteralFlags,
) -> Option<Vec<StringLiteral>> {
elements.try_fold(Vec::with_capacity(elements.len()), |mut acc, element| {
if let FStringElement::Literal(literal) = element {
if let InterpolatedStringElement::Literal(literal) = element {
acc.push(StringLiteral {
value: literal.value.clone(),
flags,
@ -162,8 +164,8 @@ mod print_arguments {
/// This function will return [`None`] if any of the arguments are not string literals,
/// or if there are no arguments at all.
fn args_to_string_literal_expr<'a>(
args: impl ExactSizeIterator<Item = &'a Vec<FStringElement>>,
sep: impl ExactSizeIterator<Item = &'a FStringElement>,
args: impl ExactSizeIterator<Item = &'a Vec<InterpolatedStringElement>>,
sep: impl ExactSizeIterator<Item = &'a InterpolatedStringElement>,
flags: StringLiteralFlags,
) -> Option<Expr> {
// If there are no arguments, short-circuit and return `None`
@ -220,8 +222,8 @@ mod print_arguments {
/// Also note that the iterator arguments of this function are consumed,
/// as opposed to the references taken by [`args_to_string_literal_expr`].
fn args_to_fstring_expr(
mut args: impl ExactSizeIterator<Item = Vec<FStringElement>>,
sep: impl ExactSizeIterator<Item = FStringElement>,
mut args: impl ExactSizeIterator<Item = Vec<InterpolatedStringElement>>,
sep: impl ExactSizeIterator<Item = InterpolatedStringElement>,
flags: FStringFlags,
) -> Option<Expr> {
// If there are no arguments, short-circuit and return `None`
@ -236,7 +238,7 @@ mod print_arguments {
Some(Expr::FString(ExprFString {
value: FStringValue::single(FString {
elements: FStringElements::from(fstring_elements),
elements: InterpolatedStringElements::from(fstring_elements),
flags,
range: TextRange::default(),
}),
@ -273,10 +275,12 @@ mod print_arguments {
)
.map(expr_to_fstring_elements)
.unwrap_or_else(|| {
vec![FStringElement::Literal(FStringLiteralElement {
range: TextRange::default(),
value: " ".into(),
})]
vec![InterpolatedStringElement::Literal(
InterpolatedStringLiteralElement {
range: TextRange::default(),
value: " ".into(),
},
)]
});
let args = arguments

View file

@ -54,11 +54,11 @@ impl AlwaysFixableViolation for ExplicitFStringTypeConversion {
/// RUF010
pub(crate) fn explicit_f_string_type_conversion(checker: &Checker, f_string: &ast::FString) {
for (index, element) in f_string.elements.iter().enumerate() {
let Some(ast::FStringExpressionElement {
let Some(ast::InterpolatedElement {
expression,
conversion,
..
}) = element.as_expression()
}) = element.as_interpolation()
else {
continue;
};

View file

@ -303,10 +303,11 @@ const fn is_valid_enclosing_node(node: AnyNodeRef) -> bool {
| AnyNodeRef::ExprYieldFrom(_)
| AnyNodeRef::ExprCompare(_)
| AnyNodeRef::ExprCall(_)
| AnyNodeRef::FStringExpressionElement(_)
| AnyNodeRef::FStringLiteralElement(_)
| AnyNodeRef::FStringFormatSpec(_)
| AnyNodeRef::InterpolatedElement(_)
| AnyNodeRef::InterpolatedStringLiteralElement(_)
| AnyNodeRef::InterpolatedStringFormatSpec(_)
| AnyNodeRef::ExprFString(_)
| AnyNodeRef::ExprTString(_)
| AnyNodeRef::ExprStringLiteral(_)
| AnyNodeRef::ExprBytesLiteral(_)
| AnyNodeRef::ExprNumberLiteral(_)
@ -344,6 +345,7 @@ const fn is_valid_enclosing_node(node: AnyNodeRef) -> bool {
| AnyNodeRef::TypeParamTypeVarTuple(_)
| AnyNodeRef::TypeParamParamSpec(_)
| AnyNodeRef::FString(_)
| AnyNodeRef::TString(_)
| AnyNodeRef::StringLiteral(_)
| AnyNodeRef::BytesLiteral(_)
| AnyNodeRef::Identifier(_) => false,

View file

@ -214,7 +214,7 @@ fn should_be_fstring(
for f_string in value.f_strings() {
let mut has_name = false;
for element in f_string.elements.expressions() {
for element in f_string.elements.interpolations() {
if let ast::Expr::Name(ast::ExprName { id, .. }) = element.expression.as_ref() {
if arg_names.contains(id) {
return false;

View file

@ -433,6 +433,18 @@ See also [JoinedStr](https://docs.python.org/3/library/ast.html#ast.JoinedStr)""
fields = [{ name = "value", type = "FStringValue" }]
custom_source_order = true
[Expr.nodes.ExprTString]
doc = """An AST node that represents either a single-part t-string literal
or an implicitly concatenated t-string literal.
This type differs from the original Python AST `TemplateStr` in that it
doesn't join the implicitly concatenated parts into a single string. Instead,
it keeps them separate and provide various methods to access the parts.
See also [TemplateStr](https://docs.python.org/3/library/ast.html#ast.TemplateStr)"""
fields = [{ name = "value", type = "TStringValue" }]
custom_source_order = true
[Expr.nodes.ExprStringLiteral]
doc = """An AST node that represents either a single-part string literal
or an implicitly concatenated string literal."""
@ -539,9 +551,10 @@ doc = "See also [excepthandler](https://docs.python.org/3/library/ast.html#ast.e
[ExceptHandler.nodes]
ExceptHandlerExceptHandler = {}
[FStringElement.nodes]
FStringExpressionElement = { variant = "Expression" }
FStringLiteralElement = { variant = "Literal" }
[InterpolatedStringElement.nodes]
InterpolatedElement = { variant = "Interpolation" }
InterpolatedStringLiteralElement = { variant = "Literal" }
[Pattern]
doc = "See also [pattern](https://docs.python.org/3/library/ast.html#ast.pattern)"
@ -565,7 +578,7 @@ TypeParamTypeVarTuple = {}
TypeParamParamSpec = {}
[ungrouped.nodes]
FStringFormatSpec = {}
InterpolatedStringFormatSpec = {}
PatternArguments = {}
PatternKeyword = {}
Comprehension = {}
@ -581,6 +594,7 @@ Decorator = {}
ElifElseClause = {}
TypeParams = {}
FString = {}
TString = {}
StringLiteral = {}
BytesLiteral = {}
Identifier = {}

View file

@ -15,7 +15,7 @@ from typing import Any
import tomllib
# Types that require `crate::`. We can slowly remove these types as we move them to generate scripts.
types_requiring_create_prefix = {
types_requiring_crate_prefix = {
"IpyEscapeKind",
"ExprContext",
"Identifier",
@ -23,6 +23,7 @@ types_requiring_create_prefix = {
"BytesLiteralValue",
"StringLiteralValue",
"FStringValue",
"TStringValue",
"Arguments",
"CmpOp",
"Comprehension",
@ -762,7 +763,7 @@ def write_node(out: list[str], ast: Ast) -> None:
ty = field.parsed_ty
rust_ty = f"{field.parsed_ty.name}"
if ty.name in types_requiring_create_prefix:
if ty.name in types_requiring_crate_prefix:
rust_ty = f"crate::{rust_ty}"
if ty.slice_:
rust_ty = f"[{rust_ty}]"

View file

@ -512,48 +512,57 @@ impl<'a> From<&'a ast::ExceptHandler> for ComparableExceptHandler<'a> {
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum ComparableFStringElement<'a> {
pub enum ComparableInterpolatedStringElement<'a> {
Literal(Cow<'a, str>),
FStringExpressionElement(FStringExpressionElement<'a>),
InterpolatedElement(InterpolatedElement<'a>),
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct FStringExpressionElement<'a> {
pub struct InterpolatedElement<'a> {
expression: ComparableExpr<'a>,
debug_text: Option<&'a ast::DebugText>,
conversion: ast::ConversionFlag,
format_spec: Option<Vec<ComparableFStringElement<'a>>>,
format_spec: Option<Vec<ComparableInterpolatedStringElement<'a>>>,
}
impl<'a> From<&'a ast::FStringElement> for ComparableFStringElement<'a> {
fn from(fstring_element: &'a ast::FStringElement) -> Self {
match fstring_element {
ast::FStringElement::Literal(ast::FStringLiteralElement { value, .. }) => {
Self::Literal(value.as_ref().into())
impl<'a> From<&'a ast::InterpolatedStringElement> for ComparableInterpolatedStringElement<'a> {
fn from(interpolated_string_element: &'a ast::InterpolatedStringElement) -> Self {
match interpolated_string_element {
ast::InterpolatedStringElement::Literal(ast::InterpolatedStringLiteralElement {
value,
..
}) => Self::Literal(value.as_ref().into()),
ast::InterpolatedStringElement::Interpolation(formatted_value) => {
formatted_value.into()
}
ast::FStringElement::Expression(formatted_value) => formatted_value.into(),
}
}
}
impl<'a> From<&'a ast::FStringExpressionElement> for ComparableFStringElement<'a> {
fn from(fstring_expression_element: &'a ast::FStringExpressionElement) -> Self {
let ast::FStringExpressionElement {
impl<'a> From<&'a ast::InterpolatedElement> for InterpolatedElement<'a> {
fn from(interpolated_element: &'a ast::InterpolatedElement) -> Self {
let ast::InterpolatedElement {
expression,
debug_text,
conversion,
format_spec,
range: _,
} = fstring_expression_element;
} = interpolated_element;
Self::FStringExpressionElement(FStringExpressionElement {
Self {
expression: (expression).into(),
debug_text: debug_text.as_ref(),
conversion: *conversion,
format_spec: format_spec
.as_ref()
.map(|spec| spec.elements.iter().map(Into::into).collect()),
})
}
}
}
impl<'a> From<&'a ast::InterpolatedElement> for ComparableInterpolatedStringElement<'a> {
fn from(interpolated_element: &'a ast::InterpolatedElement) -> Self {
Self::InterpolatedElement(interpolated_element.into())
}
}
@ -610,7 +619,7 @@ impl<'a> From<ast::LiteralExpressionRef<'a>> for ComparableLiteral<'a> {
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ComparableFString<'a> {
elements: Box<[ComparableFStringElement<'a>]>,
elements: Box<[ComparableInterpolatedStringElement<'a>]>,
}
impl<'a> From<&'a ast::FStringValue> for ComparableFString<'a> {
@ -637,7 +646,7 @@ impl<'a> From<&'a ast::FStringValue> for ComparableFString<'a> {
fn from(value: &'a ast::FStringValue) -> Self {
#[derive(Default)]
struct Collector<'a> {
elements: Vec<ComparableFStringElement<'a>>,
elements: Vec<ComparableInterpolatedStringElement<'a>>,
}
impl<'a> Collector<'a> {
@ -647,17 +656,17 @@ impl<'a> From<&'a ast::FStringValue> for ComparableFString<'a> {
// `elements` vector, while subsequent strings
// are concatenated onto this top string.
fn push_literal(&mut self, literal: &'a str) {
if let Some(ComparableFStringElement::Literal(existing_literal)) =
if let Some(ComparableInterpolatedStringElement::Literal(existing_literal)) =
self.elements.last_mut()
{
existing_literal.to_mut().push_str(literal);
} else {
self.elements
.push(ComparableFStringElement::Literal(literal.into()));
.push(ComparableInterpolatedStringElement::Literal(literal.into()));
}
}
fn push_expression(&mut self, expression: &'a ast::FStringExpressionElement) {
fn push_expression(&mut self, expression: &'a ast::InterpolatedElement) {
self.elements.push(expression.into());
}
}
@ -672,10 +681,10 @@ impl<'a> From<&'a ast::FStringValue> for ComparableFString<'a> {
ast::FStringPart::FString(fstring) => {
for element in &fstring.elements {
match element {
ast::FStringElement::Literal(literal) => {
ast::InterpolatedStringElement::Literal(literal) => {
collector.push_literal(&literal.value);
}
ast::FStringElement::Expression(expression) => {
ast::InterpolatedStringElement::Interpolation(expression) => {
collector.push_expression(expression);
}
}
@ -690,6 +699,133 @@ impl<'a> From<&'a ast::FStringValue> for ComparableFString<'a> {
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ComparableTString<'a> {
strings: Box<[ComparableInterpolatedStringElement<'a>]>,
interpolations: Box<[InterpolatedElement<'a>]>,
}
impl<'a> From<&'a ast::TStringValue> for ComparableTString<'a> {
// The approach taken below necessarily deviates from the
// corresponding implementation for [`ast::FStringValue`].
// The reason is that a t-string value is composed of _three_
// non-comparable parts: literals, f-string expressions, and
// t-string interpolations. Since we have merged the AST nodes
// that capture f-string expressions and t-string interpolations
// into the shared [`ast::InterpolatedElement`], we must
// be careful to distinguish between them here.
//
// Consequently, we model a [`ComparableTString`] on the actual
// [CPython implementation] of a `string.templatelib.Template` object:
// it is composed of `strings` and `interpolations`. In CPython,
// the `strings` field is a tuple of honest strings (since f-strings
// are evaluated). Our `strings` field will house both f-string
// expressions and string literals.
//
// Finally, as in CPython, we must be careful to ensure that the length
// of `strings` is always one more than the length of `interpolations` -
// that way we can recover the original reading order by interleaving
// starting with `strings`. This is how we can tell the
// difference between, e.g. `t"{foo}bar"` and `t"bar{foo}"`.
//
// - [CPython implementation](https://github.com/python/cpython/blob/c91ad5da9d92eac4718e4da8d53689c3cc24535e/Python/codegen.c#L4052-L4103)
fn from(value: &'a ast::TStringValue) -> Self {
struct Collector<'a> {
strings: Vec<ComparableInterpolatedStringElement<'a>>,
interpolations: Vec<InterpolatedElement<'a>>,
}
impl Default for Collector<'_> {
fn default() -> Self {
Self {
strings: vec![ComparableInterpolatedStringElement::Literal("".into())],
interpolations: vec![],
}
}
}
impl<'a> Collector<'a> {
// The logic for concatenating adjacent string literals
// occurs here, implicitly: when we encounter a sequence
// of string literals, the first gets pushed to the
// `strings` vector, while subsequent strings
// are concatenated onto this top string.
fn push_literal(&mut self, literal: &'a str) {
if let Some(ComparableInterpolatedStringElement::Literal(existing_literal)) =
self.strings.last_mut()
{
existing_literal.to_mut().push_str(literal);
} else {
self.strings
.push(ComparableInterpolatedStringElement::Literal(literal.into()));
}
}
fn start_new_literal(&mut self) {
self.strings
.push(ComparableInterpolatedStringElement::Literal("".into()));
}
fn push_fstring_expression(&mut self, expression: &'a ast::InterpolatedElement) {
if let Some(ComparableInterpolatedStringElement::Literal(last_literal)) =
self.strings.last()
{
// Recall that we insert empty strings after
// each interpolation. If we encounter an f-string
// expression, we replace the empty string with it.
if last_literal.is_empty() {
self.strings.pop();
}
}
self.strings.push(expression.into());
}
fn push_tstring_interpolation(&mut self, expression: &'a ast::InterpolatedElement) {
self.interpolations.push(expression.into());
self.start_new_literal();
}
}
let mut collector = Collector::default();
for part in value {
match part {
ast::TStringPart::Literal(string_literal) => {
collector.push_literal(&string_literal.value);
}
ast::TStringPart::TString(fstring) => {
for element in &fstring.elements {
match element {
ast::InterpolatedStringElement::Literal(literal) => {
collector.push_literal(&literal.value);
}
ast::InterpolatedStringElement::Interpolation(interpolation) => {
collector.push_tstring_interpolation(interpolation);
}
}
}
}
ast::TStringPart::FString(fstring) => {
for element in &fstring.elements {
match element {
ast::InterpolatedStringElement::Literal(literal) => {
collector.push_literal(&literal.value);
}
ast::InterpolatedStringElement::Interpolation(expression) => {
collector.push_fstring_expression(expression);
}
}
}
}
}
}
Self {
strings: collector.strings.into_boxed_slice(),
interpolations: collector.interpolations.into_boxed_slice(),
}
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ComparableStringLiteral<'a> {
value: &'a str,
@ -833,11 +969,11 @@ pub struct ExprCall<'a> {
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ExprFStringExpressionElement<'a> {
pub struct ExprInterpolatedElement<'a> {
value: Box<ComparableExpr<'a>>,
debug_text: Option<&'a ast::DebugText>,
conversion: ast::ConversionFlag,
format_spec: Vec<ComparableFStringElement<'a>>,
format_spec: Vec<ComparableInterpolatedStringElement<'a>>,
}
#[derive(Debug, PartialEq, Eq, Hash)]
@ -845,6 +981,11 @@ pub struct ExprFString<'a> {
value: ComparableFString<'a>,
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ExprTString<'a> {
value: ComparableTString<'a>,
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ExprStringLiteral<'a> {
value: ComparableStringLiteral<'a>,
@ -929,8 +1070,10 @@ pub enum ComparableExpr<'a> {
YieldFrom(ExprYieldFrom<'a>),
Compare(ExprCompare<'a>),
Call(ExprCall<'a>),
FStringExpressionElement(ExprFStringExpressionElement<'a>),
FStringExpressionElement(ExprInterpolatedElement<'a>),
FString(ExprFString<'a>),
TStringInterpolationElement(ExprInterpolatedElement<'a>),
TString(ExprTString<'a>),
StringLiteral(ExprStringLiteral<'a>),
BytesLiteral(ExprBytesLiteral<'a>),
NumberLiteral(ExprNumberLiteral<'a>),
@ -1089,6 +1232,11 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> {
value: value.into(),
})
}
ast::Expr::TString(ast::ExprTString { value, range: _ }) => {
Self::TString(ExprTString {
value: value.into(),
})
}
ast::Expr::StringLiteral(ast::ExprStringLiteral { value, range: _ }) => {
Self::StringLiteral(ExprStringLiteral {
value: ComparableStringLiteral {

View file

@ -4,7 +4,7 @@ use ruff_text_size::{Ranged, TextRange};
use crate::{
self as ast, AnyNodeRef, AnyStringFlags, Expr, ExprBytesLiteral, ExprFString, ExprRef,
ExprStringLiteral, StringFlags,
ExprStringLiteral, ExprTString, StringFlags,
};
impl<'a> From<&'a Box<Expr>> for ExprRef<'a> {
@ -80,17 +80,18 @@ impl LiteralExpressionRef<'_> {
}
/// An enum that holds a reference to a string-like expression from the AST. This includes string
/// literals, bytes literals, and f-strings.
/// literals, bytes literals, f-strings, and t-strings.
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum StringLike<'a> {
String(&'a ast::ExprStringLiteral),
Bytes(&'a ast::ExprBytesLiteral),
FString(&'a ast::ExprFString),
TString(&'a ast::ExprTString),
}
impl<'a> StringLike<'a> {
pub const fn is_fstring(self) -> bool {
matches!(self, Self::FString(_))
pub const fn is_interpolated_string(self) -> bool {
matches!(self, Self::TString(_) | Self::FString(_))
}
/// Returns an iterator over the [`StringLikePart`] contained in this string-like expression.
@ -99,6 +100,7 @@ impl<'a> StringLike<'a> {
StringLike::String(expr) => StringLikePartIter::String(expr.value.iter()),
StringLike::Bytes(expr) => StringLikePartIter::Bytes(expr.value.iter()),
StringLike::FString(expr) => StringLikePartIter::FString(expr.value.iter()),
StringLike::TString(expr) => StringLikePartIter::TString(expr.value.iter()),
}
}
@ -108,6 +110,7 @@ impl<'a> StringLike<'a> {
Self::String(ExprStringLiteral { value, .. }) => value.is_implicit_concatenated(),
Self::Bytes(ExprBytesLiteral { value, .. }) => value.is_implicit_concatenated(),
Self::FString(ExprFString { value, .. }) => value.is_implicit_concatenated(),
Self::TString(ExprTString { value, .. }) => value.is_implicit_concatenated(),
}
}
@ -116,6 +119,7 @@ impl<'a> StringLike<'a> {
StringLike::String(expr) => ExprRef::StringLiteral(expr),
StringLike::Bytes(expr) => ExprRef::BytesLiteral(expr),
StringLike::FString(expr) => ExprRef::FString(expr),
StringLike::TString(expr) => ExprRef::TString(expr),
}
}
}
@ -138,12 +142,19 @@ impl<'a> From<&'a ast::ExprFString> for StringLike<'a> {
}
}
impl<'a> From<&'a ast::ExprTString> for StringLike<'a> {
fn from(value: &'a ast::ExprTString) -> Self {
StringLike::TString(value)
}
}
impl<'a> From<&StringLike<'a>> for ExprRef<'a> {
fn from(value: &StringLike<'a>) -> Self {
match value {
StringLike::String(expr) => ExprRef::StringLiteral(expr),
StringLike::Bytes(expr) => ExprRef::BytesLiteral(expr),
StringLike::FString(expr) => ExprRef::FString(expr),
StringLike::TString(expr) => ExprRef::TString(expr),
}
}
}
@ -160,6 +171,7 @@ impl<'a> From<&StringLike<'a>> for AnyNodeRef<'a> {
StringLike::String(expr) => AnyNodeRef::ExprStringLiteral(expr),
StringLike::Bytes(expr) => AnyNodeRef::ExprBytesLiteral(expr),
StringLike::FString(expr) => AnyNodeRef::ExprFString(expr),
StringLike::TString(expr) => AnyNodeRef::ExprTString(expr),
}
}
}
@ -172,6 +184,7 @@ impl<'a> TryFrom<&'a Expr> for StringLike<'a> {
Expr::StringLiteral(value) => Ok(Self::String(value)),
Expr::BytesLiteral(value) => Ok(Self::Bytes(value)),
Expr::FString(value) => Ok(Self::FString(value)),
Expr::TString(value) => Ok(Self::TString(value)),
_ => Err(()),
}
}
@ -185,6 +198,7 @@ impl<'a> TryFrom<AnyNodeRef<'a>> for StringLike<'a> {
AnyNodeRef::ExprStringLiteral(value) => Ok(Self::String(value)),
AnyNodeRef::ExprBytesLiteral(value) => Ok(Self::Bytes(value)),
AnyNodeRef::ExprFString(value) => Ok(Self::FString(value)),
AnyNodeRef::ExprTString(value) => Ok(Self::TString(value)),
_ => Err(()),
}
}
@ -196,6 +210,7 @@ impl Ranged for StringLike<'_> {
StringLike::String(literal) => literal.range(),
StringLike::Bytes(literal) => literal.range(),
StringLike::FString(literal) => literal.range(),
StringLike::TString(literal) => literal.range(),
}
}
}
@ -206,6 +221,7 @@ pub enum StringLikePart<'a> {
String(&'a ast::StringLiteral),
Bytes(&'a ast::BytesLiteral),
FString(&'a ast::FString),
TString(&'a ast::TString),
}
impl<'a> StringLikePart<'a> {
@ -215,6 +231,7 @@ impl<'a> StringLikePart<'a> {
StringLikePart::String(string) => AnyStringFlags::from(string.flags),
StringLikePart::Bytes(bytes) => AnyStringFlags::from(bytes.flags),
StringLikePart::FString(f_string) => AnyStringFlags::from(f_string.flags),
StringLikePart::TString(t_string) => AnyStringFlags::from(t_string.flags),
}
}
@ -238,8 +255,8 @@ impl<'a> StringLikePart<'a> {
}
}
pub const fn is_fstring(self) -> bool {
matches!(self, Self::FString(_))
pub const fn is_interpolated_string(self) -> bool {
matches!(self, Self::FString(_) | Self::TString(_))
}
}
@ -261,6 +278,12 @@ impl<'a> From<&'a ast::FString> for StringLikePart<'a> {
}
}
impl<'a> From<&'a ast::TString> for StringLikePart<'a> {
fn from(value: &'a ast::TString) -> Self {
StringLikePart::TString(value)
}
}
impl<'a> From<&StringLikePart<'a>> for AnyNodeRef<'a> {
fn from(value: &StringLikePart<'a>) -> Self {
AnyNodeRef::from(*value)
@ -273,6 +296,7 @@ impl<'a> From<StringLikePart<'a>> for AnyNodeRef<'a> {
StringLikePart::String(part) => AnyNodeRef::StringLiteral(part),
StringLikePart::Bytes(part) => AnyNodeRef::BytesLiteral(part),
StringLikePart::FString(part) => AnyNodeRef::FString(part),
StringLikePart::TString(part) => AnyNodeRef::TString(part),
}
}
}
@ -283,6 +307,7 @@ impl Ranged for StringLikePart<'_> {
StringLikePart::String(part) => part.range(),
StringLikePart::Bytes(part) => part.range(),
StringLikePart::FString(part) => part.range(),
StringLikePart::TString(part) => part.range(),
}
}
}
@ -295,6 +320,7 @@ pub enum StringLikePartIter<'a> {
String(std::slice::Iter<'a, ast::StringLiteral>),
Bytes(std::slice::Iter<'a, ast::BytesLiteral>),
FString(std::slice::Iter<'a, ast::FStringPart>),
TString(std::slice::Iter<'a, ast::TStringPart>),
}
impl<'a> Iterator for StringLikePartIter<'a> {
@ -313,6 +339,16 @@ impl<'a> Iterator for StringLikePartIter<'a> {
ast::FStringPart::FString(f_string) => StringLikePart::FString(f_string),
}
}
StringLikePartIter::TString(inner) => {
let part = inner.next()?;
match part {
ast::TStringPart::Literal(string_literal) => {
StringLikePart::String(string_literal)
}
ast::TStringPart::TString(t_string) => StringLikePart::TString(t_string),
ast::TStringPart::FString(f_string) => StringLikePart::FString(f_string),
}
}
};
Some(part)
@ -323,6 +359,7 @@ impl<'a> Iterator for StringLikePartIter<'a> {
StringLikePartIter::String(inner) => inner.size_hint(),
StringLikePartIter::Bytes(inner) => inner.size_hint(),
StringLikePartIter::FString(inner) => inner.size_hint(),
StringLikePartIter::TString(inner) => inner.size_hint(),
}
}
}
@ -341,6 +378,16 @@ impl DoubleEndedIterator for StringLikePartIter<'_> {
ast::FStringPart::FString(f_string) => StringLikePart::FString(f_string),
}
}
StringLikePartIter::TString(inner) => {
let part = inner.next_back()?;
match part {
ast::TStringPart::Literal(string_literal) => {
StringLikePart::String(string_literal)
}
ast::TStringPart::TString(t_string) => StringLikePart::TString(t_string),
ast::TStringPart::FString(f_string) => StringLikePart::FString(f_string),
}
}
};
Some(part)

View file

@ -1270,6 +1270,7 @@ pub enum Expr {
Compare(crate::ExprCompare),
Call(crate::ExprCall),
FString(crate::ExprFString),
TString(crate::ExprTString),
StringLiteral(crate::ExprStringLiteral),
BytesLiteral(crate::ExprBytesLiteral),
NumberLiteral(crate::ExprNumberLiteral),
@ -1394,6 +1395,12 @@ impl From<crate::ExprFString> for Expr {
}
}
impl From<crate::ExprTString> for Expr {
fn from(node: crate::ExprTString) -> Self {
Self::TString(node)
}
}
impl From<crate::ExprStringLiteral> for Expr {
fn from(node: crate::ExprStringLiteral) -> Self {
Self::StringLiteral(node)
@ -1499,6 +1506,7 @@ impl ruff_text_size::Ranged for Expr {
Self::Compare(node) => node.range(),
Self::Call(node) => node.range(),
Self::FString(node) => node.range(),
Self::TString(node) => node.range(),
Self::StringLiteral(node) => node.range(),
Self::BytesLiteral(node) => node.range(),
Self::NumberLiteral(node) => node.range(),
@ -2185,6 +2193,43 @@ impl Expr {
}
}
#[inline]
pub const fn is_t_string_expr(&self) -> bool {
matches!(self, Self::TString(_))
}
#[inline]
pub fn t_string_expr(self) -> Option<crate::ExprTString> {
match self {
Self::TString(val) => Some(val),
_ => None,
}
}
#[inline]
pub fn expect_t_string_expr(self) -> crate::ExprTString {
match self {
Self::TString(val) => val,
_ => panic!("called expect on {self:?}"),
}
}
#[inline]
pub fn as_t_string_expr_mut(&mut self) -> Option<&mut crate::ExprTString> {
match self {
Self::TString(val) => Some(val),
_ => None,
}
}
#[inline]
pub fn as_t_string_expr(&self) -> Option<&crate::ExprTString> {
match self {
Self::TString(val) => Some(val),
_ => None,
}
}
#[inline]
pub const fn is_string_literal_expr(&self) -> bool {
matches!(self, Self::StringLiteral(_))
@ -2761,67 +2806,67 @@ impl ExceptHandler {
}
#[derive(Clone, Debug, PartialEq)]
pub enum FStringElement {
Expression(crate::FStringExpressionElement),
Literal(crate::FStringLiteralElement),
pub enum InterpolatedStringElement {
Interpolation(crate::InterpolatedElement),
Literal(crate::InterpolatedStringLiteralElement),
}
impl From<crate::FStringExpressionElement> for FStringElement {
fn from(node: crate::FStringExpressionElement) -> Self {
Self::Expression(node)
impl From<crate::InterpolatedElement> for InterpolatedStringElement {
fn from(node: crate::InterpolatedElement) -> Self {
Self::Interpolation(node)
}
}
impl From<crate::FStringLiteralElement> for FStringElement {
fn from(node: crate::FStringLiteralElement) -> Self {
impl From<crate::InterpolatedStringLiteralElement> for InterpolatedStringElement {
fn from(node: crate::InterpolatedStringLiteralElement) -> Self {
Self::Literal(node)
}
}
impl ruff_text_size::Ranged for FStringElement {
impl ruff_text_size::Ranged for InterpolatedStringElement {
fn range(&self) -> ruff_text_size::TextRange {
match self {
Self::Expression(node) => node.range(),
Self::Interpolation(node) => node.range(),
Self::Literal(node) => node.range(),
}
}
}
#[allow(dead_code, clippy::match_wildcard_for_single_variants)]
impl FStringElement {
impl InterpolatedStringElement {
#[inline]
pub const fn is_expression(&self) -> bool {
matches!(self, Self::Expression(_))
pub const fn is_interpolation(&self) -> bool {
matches!(self, Self::Interpolation(_))
}
#[inline]
pub fn expression(self) -> Option<crate::FStringExpressionElement> {
pub fn interpolation(self) -> Option<crate::InterpolatedElement> {
match self {
Self::Expression(val) => Some(val),
Self::Interpolation(val) => Some(val),
_ => None,
}
}
#[inline]
pub fn expect_expression(self) -> crate::FStringExpressionElement {
pub fn expect_interpolation(self) -> crate::InterpolatedElement {
match self {
Self::Expression(val) => val,
Self::Interpolation(val) => val,
_ => panic!("called expect on {self:?}"),
}
}
#[inline]
pub fn as_expression_mut(&mut self) -> Option<&mut crate::FStringExpressionElement> {
pub fn as_interpolation_mut(&mut self) -> Option<&mut crate::InterpolatedElement> {
match self {
Self::Expression(val) => Some(val),
Self::Interpolation(val) => Some(val),
_ => None,
}
}
#[inline]
pub fn as_expression(&self) -> Option<&crate::FStringExpressionElement> {
pub fn as_interpolation(&self) -> Option<&crate::InterpolatedElement> {
match self {
Self::Expression(val) => Some(val),
Self::Interpolation(val) => Some(val),
_ => None,
}
}
@ -2832,7 +2877,7 @@ impl FStringElement {
}
#[inline]
pub fn literal(self) -> Option<crate::FStringLiteralElement> {
pub fn literal(self) -> Option<crate::InterpolatedStringLiteralElement> {
match self {
Self::Literal(val) => Some(val),
_ => None,
@ -2840,7 +2885,7 @@ impl FStringElement {
}
#[inline]
pub fn expect_literal(self) -> crate::FStringLiteralElement {
pub fn expect_literal(self) -> crate::InterpolatedStringLiteralElement {
match self {
Self::Literal(val) => val,
_ => panic!("called expect on {self:?}"),
@ -2848,7 +2893,7 @@ impl FStringElement {
}
#[inline]
pub fn as_literal_mut(&mut self) -> Option<&mut crate::FStringLiteralElement> {
pub fn as_literal_mut(&mut self) -> Option<&mut crate::InterpolatedStringLiteralElement> {
match self {
Self::Literal(val) => Some(val),
_ => None,
@ -2856,7 +2901,7 @@ impl FStringElement {
}
#[inline]
pub fn as_literal(&self) -> Option<&crate::FStringLiteralElement> {
pub fn as_literal(&self) -> Option<&crate::InterpolatedStringLiteralElement> {
match self {
Self::Literal(val) => Some(val),
_ => None,
@ -3659,6 +3704,12 @@ impl ruff_text_size::Ranged for crate::ExprFString {
}
}
impl ruff_text_size::Ranged for crate::ExprTString {
fn range(&self) -> ruff_text_size::TextRange {
self.range
}
}
impl ruff_text_size::Ranged for crate::ExprStringLiteral {
fn range(&self) -> ruff_text_size::TextRange {
self.range
@ -3749,13 +3800,13 @@ impl ruff_text_size::Ranged for crate::ExceptHandlerExceptHandler {
}
}
impl ruff_text_size::Ranged for crate::FStringExpressionElement {
impl ruff_text_size::Ranged for crate::InterpolatedElement {
fn range(&self) -> ruff_text_size::TextRange {
self.range
}
}
impl ruff_text_size::Ranged for crate::FStringLiteralElement {
impl ruff_text_size::Ranged for crate::InterpolatedStringLiteralElement {
fn range(&self) -> ruff_text_size::TextRange {
self.range
}
@ -3827,7 +3878,7 @@ impl ruff_text_size::Ranged for crate::TypeParamParamSpec {
}
}
impl ruff_text_size::Ranged for crate::FStringFormatSpec {
impl ruff_text_size::Ranged for crate::InterpolatedStringFormatSpec {
fn range(&self) -> ruff_text_size::TextRange {
self.range
}
@ -3923,6 +3974,12 @@ impl ruff_text_size::Ranged for crate::FString {
}
}
impl ruff_text_size::Ranged for crate::TString {
fn range(&self) -> ruff_text_size::TextRange {
self.range
}
}
impl ruff_text_size::Ranged for crate::StringLiteral {
fn range(&self) -> ruff_text_size::TextRange {
self.range
@ -4015,6 +4072,7 @@ impl Expr {
Expr::Compare(node) => node.visit_source_order(visitor),
Expr::Call(node) => node.visit_source_order(visitor),
Expr::FString(node) => node.visit_source_order(visitor),
Expr::TString(node) => node.visit_source_order(visitor),
Expr::StringLiteral(node) => node.visit_source_order(visitor),
Expr::BytesLiteral(node) => node.visit_source_order(visitor),
Expr::NumberLiteral(node) => node.visit_source_order(visitor),
@ -4045,15 +4103,15 @@ impl ExceptHandler {
}
}
impl FStringElement {
impl InterpolatedStringElement {
#[allow(unused)]
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
V: crate::visitor::source_order::SourceOrderVisitor<'a> + ?Sized,
{
match self {
FStringElement::Expression(node) => node.visit_source_order(visitor),
FStringElement::Literal(node) => node.visit_source_order(visitor),
InterpolatedStringElement::Interpolation(node) => node.visit_source_order(visitor),
InterpolatedStringElement::Literal(node) => node.visit_source_order(visitor),
}
}
}
@ -4436,6 +4494,8 @@ pub enum ExprRef<'a> {
Call(&'a crate::ExprCall),
#[is(name = "f_string_expr")]
FString(&'a crate::ExprFString),
#[is(name = "t_string_expr")]
TString(&'a crate::ExprTString),
#[is(name = "string_literal_expr")]
StringLiteral(&'a crate::ExprStringLiteral),
#[is(name = "bytes_literal_expr")]
@ -4487,6 +4547,7 @@ impl<'a> From<&'a Expr> for ExprRef<'a> {
Expr::Compare(node) => ExprRef::Compare(node),
Expr::Call(node) => ExprRef::Call(node),
Expr::FString(node) => ExprRef::FString(node),
Expr::TString(node) => ExprRef::TString(node),
Expr::StringLiteral(node) => ExprRef::StringLiteral(node),
Expr::BytesLiteral(node) => ExprRef::BytesLiteral(node),
Expr::NumberLiteral(node) => ExprRef::NumberLiteral(node),
@ -4613,6 +4674,12 @@ impl<'a> From<&'a crate::ExprFString> for ExprRef<'a> {
}
}
impl<'a> From<&'a crate::ExprTString> for ExprRef<'a> {
fn from(node: &'a crate::ExprTString) -> Self {
Self::TString(node)
}
}
impl<'a> From<&'a crate::ExprStringLiteral> for ExprRef<'a> {
fn from(node: &'a crate::ExprStringLiteral) -> Self {
Self::StringLiteral(node)
@ -4718,6 +4785,7 @@ impl ruff_text_size::Ranged for ExprRef<'_> {
Self::Compare(node) => node.range(),
Self::Call(node) => node.range(),
Self::FString(node) => node.range(),
Self::TString(node) => node.range(),
Self::StringLiteral(node) => node.range(),
Self::BytesLiteral(node) => node.range(),
Self::NumberLiteral(node) => node.range(),
@ -4765,36 +4833,38 @@ impl ruff_text_size::Ranged for ExceptHandlerRef<'_> {
}
#[derive(Clone, Copy, Debug, PartialEq, is_macro::Is)]
pub enum FStringElementRef<'a> {
Expression(&'a crate::FStringExpressionElement),
Literal(&'a crate::FStringLiteralElement),
pub enum InterpolatedStringElementRef<'a> {
Interpolation(&'a crate::InterpolatedElement),
Literal(&'a crate::InterpolatedStringLiteralElement),
}
impl<'a> From<&'a FStringElement> for FStringElementRef<'a> {
fn from(node: &'a FStringElement) -> Self {
impl<'a> From<&'a InterpolatedStringElement> for InterpolatedStringElementRef<'a> {
fn from(node: &'a InterpolatedStringElement) -> Self {
match node {
FStringElement::Expression(node) => FStringElementRef::Expression(node),
FStringElement::Literal(node) => FStringElementRef::Literal(node),
InterpolatedStringElement::Interpolation(node) => {
InterpolatedStringElementRef::Interpolation(node)
}
InterpolatedStringElement::Literal(node) => InterpolatedStringElementRef::Literal(node),
}
}
}
impl<'a> From<&'a crate::FStringExpressionElement> for FStringElementRef<'a> {
fn from(node: &'a crate::FStringExpressionElement) -> Self {
Self::Expression(node)
impl<'a> From<&'a crate::InterpolatedElement> for InterpolatedStringElementRef<'a> {
fn from(node: &'a crate::InterpolatedElement) -> Self {
Self::Interpolation(node)
}
}
impl<'a> From<&'a crate::FStringLiteralElement> for FStringElementRef<'a> {
fn from(node: &'a crate::FStringLiteralElement) -> Self {
impl<'a> From<&'a crate::InterpolatedStringLiteralElement> for InterpolatedStringElementRef<'a> {
fn from(node: &'a crate::InterpolatedStringLiteralElement) -> Self {
Self::Literal(node)
}
}
impl ruff_text_size::Ranged for FStringElementRef<'_> {
impl ruff_text_size::Ranged for InterpolatedStringElementRef<'_> {
fn range(&self) -> ruff_text_size::TextRange {
match self {
Self::Expression(node) => node.range(),
Self::Interpolation(node) => node.range(),
Self::Literal(node) => node.range(),
}
}
@ -4984,6 +5054,7 @@ pub enum AnyNodeRef<'a> {
ExprCompare(&'a crate::ExprCompare),
ExprCall(&'a crate::ExprCall),
ExprFString(&'a crate::ExprFString),
ExprTString(&'a crate::ExprTString),
ExprStringLiteral(&'a crate::ExprStringLiteral),
ExprBytesLiteral(&'a crate::ExprBytesLiteral),
ExprNumberLiteral(&'a crate::ExprNumberLiteral),
@ -4999,8 +5070,8 @@ pub enum AnyNodeRef<'a> {
ExprSlice(&'a crate::ExprSlice),
ExprIpyEscapeCommand(&'a crate::ExprIpyEscapeCommand),
ExceptHandlerExceptHandler(&'a crate::ExceptHandlerExceptHandler),
FStringExpressionElement(&'a crate::FStringExpressionElement),
FStringLiteralElement(&'a crate::FStringLiteralElement),
InterpolatedElement(&'a crate::InterpolatedElement),
InterpolatedStringLiteralElement(&'a crate::InterpolatedStringLiteralElement),
PatternMatchValue(&'a crate::PatternMatchValue),
PatternMatchSingleton(&'a crate::PatternMatchSingleton),
PatternMatchSequence(&'a crate::PatternMatchSequence),
@ -5012,7 +5083,7 @@ pub enum AnyNodeRef<'a> {
TypeParamTypeVar(&'a crate::TypeParamTypeVar),
TypeParamTypeVarTuple(&'a crate::TypeParamTypeVarTuple),
TypeParamParamSpec(&'a crate::TypeParamParamSpec),
FStringFormatSpec(&'a crate::FStringFormatSpec),
InterpolatedStringFormatSpec(&'a crate::InterpolatedStringFormatSpec),
PatternArguments(&'a crate::PatternArguments),
PatternKeyword(&'a crate::PatternKeyword),
Comprehension(&'a crate::Comprehension),
@ -5028,6 +5099,7 @@ pub enum AnyNodeRef<'a> {
ElifElseClause(&'a crate::ElifElseClause),
TypeParams(&'a crate::TypeParams),
FString(&'a crate::FString),
TString(&'a crate::TString),
StringLiteral(&'a crate::StringLiteral),
BytesLiteral(&'a crate::BytesLiteral),
Identifier(&'a crate::Identifier),
@ -5181,6 +5253,7 @@ impl<'a> From<&'a Expr> for AnyNodeRef<'a> {
Expr::Compare(node) => AnyNodeRef::ExprCompare(node),
Expr::Call(node) => AnyNodeRef::ExprCall(node),
Expr::FString(node) => AnyNodeRef::ExprFString(node),
Expr::TString(node) => AnyNodeRef::ExprTString(node),
Expr::StringLiteral(node) => AnyNodeRef::ExprStringLiteral(node),
Expr::BytesLiteral(node) => AnyNodeRef::ExprBytesLiteral(node),
Expr::NumberLiteral(node) => AnyNodeRef::ExprNumberLiteral(node),
@ -5220,6 +5293,7 @@ impl<'a> From<ExprRef<'a>> for AnyNodeRef<'a> {
ExprRef::Compare(node) => AnyNodeRef::ExprCompare(node),
ExprRef::Call(node) => AnyNodeRef::ExprCall(node),
ExprRef::FString(node) => AnyNodeRef::ExprFString(node),
ExprRef::TString(node) => AnyNodeRef::ExprTString(node),
ExprRef::StringLiteral(node) => AnyNodeRef::ExprStringLiteral(node),
ExprRef::BytesLiteral(node) => AnyNodeRef::ExprBytesLiteral(node),
ExprRef::NumberLiteral(node) => AnyNodeRef::ExprNumberLiteral(node),
@ -5259,6 +5333,7 @@ impl<'a> AnyNodeRef<'a> {
Self::ExprCompare(node) => Some(ExprRef::Compare(node)),
Self::ExprCall(node) => Some(ExprRef::Call(node)),
Self::ExprFString(node) => Some(ExprRef::FString(node)),
Self::ExprTString(node) => Some(ExprRef::TString(node)),
Self::ExprStringLiteral(node) => Some(ExprRef::StringLiteral(node)),
Self::ExprBytesLiteral(node) => Some(ExprRef::BytesLiteral(node)),
Self::ExprNumberLiteral(node) => Some(ExprRef::NumberLiteral(node)),
@ -5305,29 +5380,39 @@ impl<'a> AnyNodeRef<'a> {
}
}
impl<'a> From<&'a FStringElement> for AnyNodeRef<'a> {
fn from(node: &'a FStringElement) -> AnyNodeRef<'a> {
impl<'a> From<&'a InterpolatedStringElement> for AnyNodeRef<'a> {
fn from(node: &'a InterpolatedStringElement) -> AnyNodeRef<'a> {
match node {
FStringElement::Expression(node) => AnyNodeRef::FStringExpressionElement(node),
FStringElement::Literal(node) => AnyNodeRef::FStringLiteralElement(node),
InterpolatedStringElement::Interpolation(node) => AnyNodeRef::InterpolatedElement(node),
InterpolatedStringElement::Literal(node) => {
AnyNodeRef::InterpolatedStringLiteralElement(node)
}
}
}
}
impl<'a> From<FStringElementRef<'a>> for AnyNodeRef<'a> {
fn from(node: FStringElementRef<'a>) -> AnyNodeRef<'a> {
impl<'a> From<InterpolatedStringElementRef<'a>> for AnyNodeRef<'a> {
fn from(node: InterpolatedStringElementRef<'a>) -> AnyNodeRef<'a> {
match node {
FStringElementRef::Expression(node) => AnyNodeRef::FStringExpressionElement(node),
FStringElementRef::Literal(node) => AnyNodeRef::FStringLiteralElement(node),
InterpolatedStringElementRef::Interpolation(node) => {
AnyNodeRef::InterpolatedElement(node)
}
InterpolatedStringElementRef::Literal(node) => {
AnyNodeRef::InterpolatedStringLiteralElement(node)
}
}
}
}
impl<'a> AnyNodeRef<'a> {
pub fn as_f_string_element_ref(self) -> Option<FStringElementRef<'a>> {
pub fn as_interpolated_string_element_ref(self) -> Option<InterpolatedStringElementRef<'a>> {
match self {
Self::FStringExpressionElement(node) => Some(FStringElementRef::Expression(node)),
Self::FStringLiteralElement(node) => Some(FStringElementRef::Literal(node)),
Self::InterpolatedElement(node) => {
Some(InterpolatedStringElementRef::Interpolation(node))
}
Self::InterpolatedStringLiteralElement(node) => {
Some(InterpolatedStringElementRef::Literal(node))
}
_ => None,
}
@ -5683,6 +5768,12 @@ impl<'a> From<&'a crate::ExprFString> for AnyNodeRef<'a> {
}
}
impl<'a> From<&'a crate::ExprTString> for AnyNodeRef<'a> {
fn from(node: &'a crate::ExprTString) -> AnyNodeRef<'a> {
AnyNodeRef::ExprTString(node)
}
}
impl<'a> From<&'a crate::ExprStringLiteral> for AnyNodeRef<'a> {
fn from(node: &'a crate::ExprStringLiteral) -> AnyNodeRef<'a> {
AnyNodeRef::ExprStringLiteral(node)
@ -5773,15 +5864,15 @@ impl<'a> From<&'a crate::ExceptHandlerExceptHandler> for AnyNodeRef<'a> {
}
}
impl<'a> From<&'a crate::FStringExpressionElement> for AnyNodeRef<'a> {
fn from(node: &'a crate::FStringExpressionElement) -> AnyNodeRef<'a> {
AnyNodeRef::FStringExpressionElement(node)
impl<'a> From<&'a crate::InterpolatedElement> for AnyNodeRef<'a> {
fn from(node: &'a crate::InterpolatedElement) -> AnyNodeRef<'a> {
AnyNodeRef::InterpolatedElement(node)
}
}
impl<'a> From<&'a crate::FStringLiteralElement> for AnyNodeRef<'a> {
fn from(node: &'a crate::FStringLiteralElement) -> AnyNodeRef<'a> {
AnyNodeRef::FStringLiteralElement(node)
impl<'a> From<&'a crate::InterpolatedStringLiteralElement> for AnyNodeRef<'a> {
fn from(node: &'a crate::InterpolatedStringLiteralElement) -> AnyNodeRef<'a> {
AnyNodeRef::InterpolatedStringLiteralElement(node)
}
}
@ -5851,9 +5942,9 @@ impl<'a> From<&'a crate::TypeParamParamSpec> for AnyNodeRef<'a> {
}
}
impl<'a> From<&'a crate::FStringFormatSpec> for AnyNodeRef<'a> {
fn from(node: &'a crate::FStringFormatSpec) -> AnyNodeRef<'a> {
AnyNodeRef::FStringFormatSpec(node)
impl<'a> From<&'a crate::InterpolatedStringFormatSpec> for AnyNodeRef<'a> {
fn from(node: &'a crate::InterpolatedStringFormatSpec) -> AnyNodeRef<'a> {
AnyNodeRef::InterpolatedStringFormatSpec(node)
}
}
@ -5947,6 +6038,12 @@ impl<'a> From<&'a crate::FString> for AnyNodeRef<'a> {
}
}
impl<'a> From<&'a crate::TString> for AnyNodeRef<'a> {
fn from(node: &'a crate::TString) -> AnyNodeRef<'a> {
AnyNodeRef::TString(node)
}
}
impl<'a> From<&'a crate::StringLiteral> for AnyNodeRef<'a> {
fn from(node: &'a crate::StringLiteral) -> AnyNodeRef<'a> {
AnyNodeRef::StringLiteral(node)
@ -6013,6 +6110,7 @@ impl ruff_text_size::Ranged for AnyNodeRef<'_> {
AnyNodeRef::ExprCompare(node) => node.range(),
AnyNodeRef::ExprCall(node) => node.range(),
AnyNodeRef::ExprFString(node) => node.range(),
AnyNodeRef::ExprTString(node) => node.range(),
AnyNodeRef::ExprStringLiteral(node) => node.range(),
AnyNodeRef::ExprBytesLiteral(node) => node.range(),
AnyNodeRef::ExprNumberLiteral(node) => node.range(),
@ -6028,8 +6126,8 @@ impl ruff_text_size::Ranged for AnyNodeRef<'_> {
AnyNodeRef::ExprSlice(node) => node.range(),
AnyNodeRef::ExprIpyEscapeCommand(node) => node.range(),
AnyNodeRef::ExceptHandlerExceptHandler(node) => node.range(),
AnyNodeRef::FStringExpressionElement(node) => node.range(),
AnyNodeRef::FStringLiteralElement(node) => node.range(),
AnyNodeRef::InterpolatedElement(node) => node.range(),
AnyNodeRef::InterpolatedStringLiteralElement(node) => node.range(),
AnyNodeRef::PatternMatchValue(node) => node.range(),
AnyNodeRef::PatternMatchSingleton(node) => node.range(),
AnyNodeRef::PatternMatchSequence(node) => node.range(),
@ -6041,7 +6139,7 @@ impl ruff_text_size::Ranged for AnyNodeRef<'_> {
AnyNodeRef::TypeParamTypeVar(node) => node.range(),
AnyNodeRef::TypeParamTypeVarTuple(node) => node.range(),
AnyNodeRef::TypeParamParamSpec(node) => node.range(),
AnyNodeRef::FStringFormatSpec(node) => node.range(),
AnyNodeRef::InterpolatedStringFormatSpec(node) => node.range(),
AnyNodeRef::PatternArguments(node) => node.range(),
AnyNodeRef::PatternKeyword(node) => node.range(),
AnyNodeRef::Comprehension(node) => node.range(),
@ -6057,6 +6155,7 @@ impl ruff_text_size::Ranged for AnyNodeRef<'_> {
AnyNodeRef::ElifElseClause(node) => node.range(),
AnyNodeRef::TypeParams(node) => node.range(),
AnyNodeRef::FString(node) => node.range(),
AnyNodeRef::TString(node) => node.range(),
AnyNodeRef::StringLiteral(node) => node.range(),
AnyNodeRef::BytesLiteral(node) => node.range(),
AnyNodeRef::Identifier(node) => node.range(),
@ -6112,6 +6211,7 @@ impl AnyNodeRef<'_> {
AnyNodeRef::ExprCompare(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::ExprCall(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::ExprFString(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::ExprTString(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::ExprStringLiteral(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::ExprBytesLiteral(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::ExprNumberLiteral(node) => std::ptr::NonNull::from(*node).cast(),
@ -6127,8 +6227,10 @@ impl AnyNodeRef<'_> {
AnyNodeRef::ExprSlice(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::ExprIpyEscapeCommand(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::ExceptHandlerExceptHandler(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::FStringExpressionElement(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::FStringLiteralElement(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::InterpolatedElement(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::InterpolatedStringLiteralElement(node) => {
std::ptr::NonNull::from(*node).cast()
}
AnyNodeRef::PatternMatchValue(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::PatternMatchSingleton(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::PatternMatchSequence(node) => std::ptr::NonNull::from(*node).cast(),
@ -6140,7 +6242,7 @@ impl AnyNodeRef<'_> {
AnyNodeRef::TypeParamTypeVar(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::TypeParamTypeVarTuple(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::TypeParamParamSpec(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::FStringFormatSpec(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::InterpolatedStringFormatSpec(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::PatternArguments(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::PatternKeyword(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::Comprehension(node) => std::ptr::NonNull::from(*node).cast(),
@ -6156,6 +6258,7 @@ impl AnyNodeRef<'_> {
AnyNodeRef::ElifElseClause(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::TypeParams(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::FString(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::TString(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::StringLiteral(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::BytesLiteral(node) => std::ptr::NonNull::from(*node).cast(),
AnyNodeRef::Identifier(node) => std::ptr::NonNull::from(*node).cast(),
@ -6215,6 +6318,7 @@ impl<'a> AnyNodeRef<'a> {
AnyNodeRef::ExprCompare(node) => node.visit_source_order(visitor),
AnyNodeRef::ExprCall(node) => node.visit_source_order(visitor),
AnyNodeRef::ExprFString(node) => node.visit_source_order(visitor),
AnyNodeRef::ExprTString(node) => node.visit_source_order(visitor),
AnyNodeRef::ExprStringLiteral(node) => node.visit_source_order(visitor),
AnyNodeRef::ExprBytesLiteral(node) => node.visit_source_order(visitor),
AnyNodeRef::ExprNumberLiteral(node) => node.visit_source_order(visitor),
@ -6230,8 +6334,8 @@ impl<'a> AnyNodeRef<'a> {
AnyNodeRef::ExprSlice(node) => node.visit_source_order(visitor),
AnyNodeRef::ExprIpyEscapeCommand(node) => node.visit_source_order(visitor),
AnyNodeRef::ExceptHandlerExceptHandler(node) => node.visit_source_order(visitor),
AnyNodeRef::FStringExpressionElement(node) => node.visit_source_order(visitor),
AnyNodeRef::FStringLiteralElement(node) => node.visit_source_order(visitor),
AnyNodeRef::InterpolatedElement(node) => node.visit_source_order(visitor),
AnyNodeRef::InterpolatedStringLiteralElement(node) => node.visit_source_order(visitor),
AnyNodeRef::PatternMatchValue(node) => node.visit_source_order(visitor),
AnyNodeRef::PatternMatchSingleton(node) => node.visit_source_order(visitor),
AnyNodeRef::PatternMatchSequence(node) => node.visit_source_order(visitor),
@ -6243,7 +6347,7 @@ impl<'a> AnyNodeRef<'a> {
AnyNodeRef::TypeParamTypeVar(node) => node.visit_source_order(visitor),
AnyNodeRef::TypeParamTypeVarTuple(node) => node.visit_source_order(visitor),
AnyNodeRef::TypeParamParamSpec(node) => node.visit_source_order(visitor),
AnyNodeRef::FStringFormatSpec(node) => node.visit_source_order(visitor),
AnyNodeRef::InterpolatedStringFormatSpec(node) => node.visit_source_order(visitor),
AnyNodeRef::PatternArguments(node) => node.visit_source_order(visitor),
AnyNodeRef::PatternKeyword(node) => node.visit_source_order(visitor),
AnyNodeRef::Comprehension(node) => node.visit_source_order(visitor),
@ -6259,6 +6363,7 @@ impl<'a> AnyNodeRef<'a> {
AnyNodeRef::ElifElseClause(node) => node.visit_source_order(visitor),
AnyNodeRef::TypeParams(node) => node.visit_source_order(visitor),
AnyNodeRef::FString(node) => node.visit_source_order(visitor),
AnyNodeRef::TString(node) => node.visit_source_order(visitor),
AnyNodeRef::StringLiteral(node) => node.visit_source_order(visitor),
AnyNodeRef::BytesLiteral(node) => node.visit_source_order(visitor),
AnyNodeRef::Identifier(node) => node.visit_source_order(visitor),
@ -6330,6 +6435,7 @@ impl AnyNodeRef<'_> {
| AnyNodeRef::ExprCompare(_)
| AnyNodeRef::ExprCall(_)
| AnyNodeRef::ExprFString(_)
| AnyNodeRef::ExprTString(_)
| AnyNodeRef::ExprStringLiteral(_)
| AnyNodeRef::ExprBytesLiteral(_)
| AnyNodeRef::ExprNumberLiteral(_)
@ -6355,10 +6461,10 @@ impl AnyNodeRef<'_> {
}
impl AnyNodeRef<'_> {
pub const fn is_f_string_element(self) -> bool {
pub const fn is_interpolated_string_element(self) -> bool {
matches!(
self,
AnyNodeRef::FStringExpressionElement(_) | AnyNodeRef::FStringLiteralElement(_)
AnyNodeRef::InterpolatedElement(_) | AnyNodeRef::InterpolatedStringLiteralElement(_)
)
}
}
@ -6437,6 +6543,7 @@ pub enum NodeKind {
ExprCompare,
ExprCall,
ExprFString,
ExprTString,
ExprStringLiteral,
ExprBytesLiteral,
ExprNumberLiteral,
@ -6452,8 +6559,8 @@ pub enum NodeKind {
ExprSlice,
ExprIpyEscapeCommand,
ExceptHandlerExceptHandler,
FStringExpressionElement,
FStringLiteralElement,
InterpolatedElement,
InterpolatedStringLiteralElement,
PatternMatchValue,
PatternMatchSingleton,
PatternMatchSequence,
@ -6465,7 +6572,7 @@ pub enum NodeKind {
TypeParamTypeVar,
TypeParamTypeVarTuple,
TypeParamParamSpec,
FStringFormatSpec,
InterpolatedStringFormatSpec,
PatternArguments,
PatternKeyword,
Comprehension,
@ -6481,6 +6588,7 @@ pub enum NodeKind {
ElifElseClause,
TypeParams,
FString,
TString,
StringLiteral,
BytesLiteral,
Identifier,
@ -6534,6 +6642,7 @@ impl AnyNodeRef<'_> {
AnyNodeRef::ExprCompare(_) => NodeKind::ExprCompare,
AnyNodeRef::ExprCall(_) => NodeKind::ExprCall,
AnyNodeRef::ExprFString(_) => NodeKind::ExprFString,
AnyNodeRef::ExprTString(_) => NodeKind::ExprTString,
AnyNodeRef::ExprStringLiteral(_) => NodeKind::ExprStringLiteral,
AnyNodeRef::ExprBytesLiteral(_) => NodeKind::ExprBytesLiteral,
AnyNodeRef::ExprNumberLiteral(_) => NodeKind::ExprNumberLiteral,
@ -6549,8 +6658,10 @@ impl AnyNodeRef<'_> {
AnyNodeRef::ExprSlice(_) => NodeKind::ExprSlice,
AnyNodeRef::ExprIpyEscapeCommand(_) => NodeKind::ExprIpyEscapeCommand,
AnyNodeRef::ExceptHandlerExceptHandler(_) => NodeKind::ExceptHandlerExceptHandler,
AnyNodeRef::FStringExpressionElement(_) => NodeKind::FStringExpressionElement,
AnyNodeRef::FStringLiteralElement(_) => NodeKind::FStringLiteralElement,
AnyNodeRef::InterpolatedElement(_) => NodeKind::InterpolatedElement,
AnyNodeRef::InterpolatedStringLiteralElement(_) => {
NodeKind::InterpolatedStringLiteralElement
}
AnyNodeRef::PatternMatchValue(_) => NodeKind::PatternMatchValue,
AnyNodeRef::PatternMatchSingleton(_) => NodeKind::PatternMatchSingleton,
AnyNodeRef::PatternMatchSequence(_) => NodeKind::PatternMatchSequence,
@ -6562,7 +6673,7 @@ impl AnyNodeRef<'_> {
AnyNodeRef::TypeParamTypeVar(_) => NodeKind::TypeParamTypeVar,
AnyNodeRef::TypeParamTypeVarTuple(_) => NodeKind::TypeParamTypeVarTuple,
AnyNodeRef::TypeParamParamSpec(_) => NodeKind::TypeParamParamSpec,
AnyNodeRef::FStringFormatSpec(_) => NodeKind::FStringFormatSpec,
AnyNodeRef::InterpolatedStringFormatSpec(_) => NodeKind::InterpolatedStringFormatSpec,
AnyNodeRef::PatternArguments(_) => NodeKind::PatternArguments,
AnyNodeRef::PatternKeyword(_) => NodeKind::PatternKeyword,
AnyNodeRef::Comprehension(_) => NodeKind::Comprehension,
@ -6578,6 +6689,7 @@ impl AnyNodeRef<'_> {
AnyNodeRef::ElifElseClause(_) => NodeKind::ElifElseClause,
AnyNodeRef::TypeParams(_) => NodeKind::TypeParams,
AnyNodeRef::FString(_) => NodeKind::FString,
AnyNodeRef::TString(_) => NodeKind::TString,
AnyNodeRef::StringLiteral(_) => NodeKind::StringLiteral,
AnyNodeRef::BytesLiteral(_) => NodeKind::BytesLiteral,
AnyNodeRef::Identifier(_) => NodeKind::Identifier,
@ -7023,6 +7135,20 @@ pub struct ExprFString {
pub value: crate::FStringValue,
}
/// An AST node that represents either a single-part t-string literal
/// or an implicitly concatenated t-string literal.
///
/// This type differs from the original Python AST `TemplateStr` in that it
/// doesn't join the implicitly concatenated parts into a single string. Instead,
/// it keeps them separate and provide various methods to access the parts.
///
/// See also [TemplateStr](https://docs.python.org/3/library/ast.html#ast.TemplateStr)
#[derive(Clone, Debug, PartialEq)]
pub struct ExprTString {
pub range: ruff_text_size::TextRange,
pub value: crate::TStringValue,
}
/// An AST node that represents either a single-part string literal
/// or an implicitly concatenated string literal.
#[derive(Clone, Debug, PartialEq)]

View file

@ -12,8 +12,8 @@ use crate::parenthesize::parenthesized_range;
use crate::statement_visitor::StatementVisitor;
use crate::visitor::Visitor;
use crate::{
self as ast, Arguments, CmpOp, DictItem, ExceptHandler, Expr, FStringElement, MatchCase,
Operator, Pattern, Stmt, TypeParam,
self as ast, Arguments, CmpOp, DictItem, ExceptHandler, Expr, InterpolatedStringElement,
MatchCase, Operator, Pattern, Stmt, TypeParam,
};
use crate::{AnyNodeRef, ExprContext};
@ -138,7 +138,10 @@ pub fn any_over_expr(expr: &Expr, func: &dyn Fn(&Expr) -> bool) -> bool {
}
Expr::FString(ast::ExprFString { value, .. }) => value
.elements()
.any(|expr| any_over_f_string_element(expr, func)),
.any(|expr| any_over_interpolated_string_element(expr, func)),
Expr::TString(ast::ExprTString { value, .. }) => value
.elements()
.any(|expr| any_over_interpolated_string_element(expr, func)),
Expr::Named(ast::ExprNamed {
target,
value,
@ -315,22 +318,22 @@ pub fn any_over_pattern(pattern: &Pattern, func: &dyn Fn(&Expr) -> bool) -> bool
}
}
pub fn any_over_f_string_element(
element: &ast::FStringElement,
pub fn any_over_interpolated_string_element(
element: &ast::InterpolatedStringElement,
func: &dyn Fn(&Expr) -> bool,
) -> bool {
match element {
ast::FStringElement::Literal(_) => false,
ast::FStringElement::Expression(ast::FStringExpressionElement {
ast::InterpolatedStringElement::Literal(_) => false,
ast::InterpolatedStringElement::Interpolation(ast::InterpolatedElement {
expression,
format_spec,
..
}) => {
any_over_expr(expression, func)
|| format_spec.as_ref().is_some_and(|spec| {
spec.elements
.iter()
.any(|spec_element| any_over_f_string_element(spec_element, func))
spec.elements.iter().any(|spec_element| {
any_over_interpolated_string_element(spec_element, func)
})
})
}
}
@ -1304,6 +1307,8 @@ fn is_non_empty_f_string(expr: &ast::ExprFString) -> bool {
// These literals may or may not be empty.
Expr::FString(f_string) => is_non_empty_f_string(f_string),
// These literals may or may not be empty.
Expr::TString(f_string) => is_non_empty_t_string(f_string),
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => !value.is_empty(),
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => !value.is_empty(),
}
@ -1313,8 +1318,78 @@ fn is_non_empty_f_string(expr: &ast::ExprFString) -> bool {
ast::FStringPart::Literal(string_literal) => !string_literal.is_empty(),
ast::FStringPart::FString(f_string) => {
f_string.elements.iter().all(|element| match element {
FStringElement::Literal(string_literal) => !string_literal.is_empty(),
FStringElement::Expression(f_string) => inner(&f_string.expression),
InterpolatedStringElement::Literal(string_literal) => !string_literal.is_empty(),
InterpolatedStringElement::Interpolation(f_string) => inner(&f_string.expression),
})
}
})
}
/// Returns `true` if the expression definitely resolves to a non-empty string, when used as an
/// f-string expression, or `false` if the expression may resolve to an empty string.
fn is_non_empty_t_string(expr: &ast::ExprTString) -> bool {
fn inner(expr: &Expr) -> bool {
match expr {
// When stringified, these expressions are always non-empty.
Expr::Lambda(_) => true,
Expr::Dict(_) => true,
Expr::Set(_) => true,
Expr::ListComp(_) => true,
Expr::SetComp(_) => true,
Expr::DictComp(_) => true,
Expr::Compare(_) => true,
Expr::NumberLiteral(_) => true,
Expr::BooleanLiteral(_) => true,
Expr::NoneLiteral(_) => true,
Expr::EllipsisLiteral(_) => true,
Expr::List(_) => true,
Expr::Tuple(_) => true,
// These expressions must resolve to the inner expression.
Expr::If(ast::ExprIf { body, orelse, .. }) => inner(body) && inner(orelse),
Expr::Named(ast::ExprNamed { value, .. }) => inner(value),
// These expressions are complex. We can't determine whether they're empty or not.
Expr::BoolOp(ast::ExprBoolOp { .. }) => false,
Expr::BinOp(ast::ExprBinOp { .. }) => false,
Expr::UnaryOp(ast::ExprUnaryOp { .. }) => false,
Expr::Generator(_) => false,
Expr::Await(_) => false,
Expr::Yield(_) => false,
Expr::YieldFrom(_) => false,
Expr::Call(_) => false,
Expr::Attribute(_) => false,
Expr::Subscript(_) => false,
Expr::Starred(_) => false,
Expr::Name(_) => false,
Expr::Slice(_) => false,
Expr::IpyEscapeCommand(_) => false,
// These literals may or may not be empty.
Expr::FString(f_string) => is_non_empty_f_string(f_string),
// These literals may or may not be empty.
Expr::TString(t_string) => is_non_empty_t_string(t_string),
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => !value.is_empty(),
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => !value.is_empty(),
}
}
expr.value.iter().any(|part| match part {
ast::TStringPart::Literal(string_literal) => !string_literal.is_empty(),
ast::TStringPart::TString(t_string) => {
t_string.elements.iter().all(|element| match element {
ast::InterpolatedStringElement::Literal(string_literal) => {
!string_literal.is_empty()
}
ast::InterpolatedStringElement::Interpolation(t_string) => {
inner(&t_string.expression)
}
})
}
ast::TStringPart::FString(f_string) => {
f_string.elements.iter().all(|element| match element {
InterpolatedStringElement::Literal(string_literal) => !string_literal.is_empty(),
InterpolatedStringElement::Interpolation(f_string) => inner(&f_string.expression),
})
}
})
@ -1331,10 +1406,10 @@ fn is_empty_f_string(expr: &ast::ExprFString) -> bool {
value
.elements()
.all(|f_string_element| match f_string_element {
FStringElement::Literal(ast::FStringLiteralElement { value, .. }) => {
value.is_empty()
}
FStringElement::Expression(ast::FStringExpressionElement {
InterpolatedStringElement::Literal(
ast::InterpolatedStringLiteralElement { value, .. },
) => value.is_empty(),
InterpolatedStringElement::Interpolation(ast::InterpolatedElement {
expression,
..
}) => inner(expression),
@ -1348,8 +1423,8 @@ fn is_empty_f_string(expr: &ast::ExprFString) -> bool {
ast::FStringPart::Literal(string_literal) => string_literal.is_empty(),
ast::FStringPart::FString(f_string) => {
f_string.elements.iter().all(|element| match element {
FStringElement::Literal(string_literal) => string_literal.is_empty(),
FStringElement::Expression(f_string) => inner(&f_string.expression),
InterpolatedStringElement::Literal(string_literal) => string_literal.is_empty(),
InterpolatedStringElement::Interpolation(f_string) => inner(&f_string.expression),
})
}
})

View file

@ -85,23 +85,23 @@ impl ast::ExprCompare {
}
}
impl ast::FStringFormatSpec {
impl ast::InterpolatedStringFormatSpec {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
for element in &self.elements {
visitor.visit_f_string_element(element);
visitor.visit_interpolated_string_element(element);
}
}
}
impl ast::FStringExpressionElement {
impl ast::InterpolatedElement {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
let ast::FStringExpressionElement {
let ast::InterpolatedElement {
expression,
format_spec,
..
@ -110,18 +110,18 @@ impl ast::FStringExpressionElement {
if let Some(format_spec) = format_spec {
for spec_part in &format_spec.elements {
visitor.visit_f_string_element(spec_part);
visitor.visit_interpolated_string_element(spec_part);
}
}
}
}
impl ast::FStringLiteralElement {
impl ast::InterpolatedStringLiteralElement {
pub(crate) fn visit_source_order<'a, V>(&'a self, _visitor: &mut V)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
let ast::FStringLiteralElement { range: _, value: _ } = self;
let ast::InterpolatedStringLiteralElement { range: _, value: _ } = self;
}
}
@ -145,6 +145,29 @@ impl ast::ExprFString {
}
}
impl ast::ExprTString {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
let ast::ExprTString { value, range: _ } = self;
for t_string_part in value {
match t_string_part {
ast::TStringPart::Literal(string_literal) => {
visitor.visit_string_literal(string_literal);
}
ast::TStringPart::FString(f_string) => {
visitor.visit_f_string(f_string);
}
ast::TStringPart::TString(t_string) => {
visitor.visit_t_string(t_string);
}
}
}
}
}
impl ast::ExprStringLiteral {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
@ -615,7 +638,24 @@ impl ast::FString {
} = self;
for fstring_element in elements {
visitor.visit_f_string_element(fstring_element);
visitor.visit_interpolated_string_element(fstring_element);
}
}
}
impl ast::TString {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
let ast::TString {
elements,
range: _,
flags: _,
} = self;
for tstring_element in elements {
visitor.visit_interpolated_string_element(tstring_element);
}
}
}

View file

@ -2,7 +2,7 @@
use crate::generated::{
ExprBytesLiteral, ExprDict, ExprFString, ExprList, ExprName, ExprSet, ExprStringLiteral,
ExprTuple, StmtClassDef,
ExprTString, ExprTuple, StmtClassDef,
};
use std::borrow::Cow;
use std::fmt;
@ -17,10 +17,12 @@ use itertools::Itertools;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::str_prefix::{AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix};
use crate::str_prefix::{
AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix, TStringPrefix,
};
use crate::{
Expr, ExprRef, FStringElement, LiteralExpressionRef, OperatorPrecedence, Pattern, Stmt,
TypeParam, int,
Expr, ExprRef, InterpolatedStringElement, LiteralExpressionRef, OperatorPrecedence, Pattern,
Stmt, TypeParam, int,
name::Name,
str::{Quote, TripleQuotes},
};
@ -312,35 +314,35 @@ impl<'a> IntoIterator for &'a ExprSet {
}
#[derive(Clone, Debug, PartialEq)]
pub struct FStringFormatSpec {
pub struct InterpolatedStringFormatSpec {
pub range: TextRange,
pub elements: FStringElements,
pub elements: InterpolatedStringElements,
}
/// See also [FormattedValue](https://docs.python.org/3/library/ast.html#ast.FormattedValue)
#[derive(Clone, Debug, PartialEq)]
pub struct FStringExpressionElement {
pub struct InterpolatedElement {
pub range: TextRange,
pub expression: Box<Expr>,
pub debug_text: Option<DebugText>,
pub conversion: ConversionFlag,
pub format_spec: Option<Box<FStringFormatSpec>>,
pub format_spec: Option<Box<InterpolatedStringFormatSpec>>,
}
/// An `FStringLiteralElement` with an empty `value` is an invalid f-string element.
#[derive(Clone, Debug, PartialEq)]
pub struct FStringLiteralElement {
pub struct InterpolatedStringLiteralElement {
pub range: TextRange,
pub value: Box<str>,
}
impl FStringLiteralElement {
impl InterpolatedStringLiteralElement {
pub fn is_valid(&self) -> bool {
!self.value.is_empty()
}
}
impl Deref for FStringLiteralElement {
impl Deref for InterpolatedStringLiteralElement {
type Target = str;
fn deref(&self) -> &Self::Target {
@ -483,7 +485,7 @@ impl FStringValue {
self.iter().filter_map(|part| part.as_f_string())
}
/// Returns an iterator over all the [`FStringElement`] contained in this value.
/// Returns an iterator over all the [`InterpolatedStringElement`] contained in this value.
///
/// An f-string element is what makes up an [`FString`] i.e., it is either a
/// string literal or an expression. In the following example,
@ -494,7 +496,7 @@ impl FStringValue {
///
/// The f-string elements returned would be string literal (`"bar "`),
/// expression (`x`) and string literal (`"qux"`).
pub fn elements(&self) -> impl Iterator<Item = &FStringElement> {
pub fn elements(&self) -> impl Iterator<Item = &InterpolatedStringElement> {
self.f_strings().flat_map(|fstring| fstring.elements.iter())
}
}
@ -554,6 +556,181 @@ impl Ranged for FStringPart {
}
}
impl ExprTString {
/// Returns the single [`TString`] if the t-string isn't implicitly concatenated, [`None`]
/// otherwise.
pub const fn as_single_part_tstring(&self) -> Option<&TString> {
match &self.value.inner {
TStringValueInner::Single(TStringPart::TString(tstring)) => Some(tstring),
_ => None,
}
}
}
/// The value representing an [`ExprTString`].
#[derive(Clone, Debug, PartialEq)]
pub struct TStringValue {
inner: TStringValueInner,
}
impl TStringValue {
/// Creates a new t-string literal with a single [`TString`] part.
pub fn single(value: TString) -> Self {
Self {
inner: TStringValueInner::Single(TStringPart::TString(value)),
}
}
/// Creates a new t-string with the given values that represents an implicitly
/// concatenated t-string.
///
/// # Panics
///
/// Panics if `values` has less than 2 elements.
/// Use [`TStringValue::single`] instead.
pub fn concatenated(values: Vec<TStringPart>) -> Self {
assert!(
values.len() > 1,
"Use `TStringValue::single` to create single-part t-strings"
);
Self {
inner: TStringValueInner::Concatenated(values),
}
}
/// Returns `true` if the t-string is implicitly concatenated, `false` otherwise.
pub fn is_implicit_concatenated(&self) -> bool {
matches!(self.inner, TStringValueInner::Concatenated(_))
}
/// Returns a slice of all the [`TStringPart`]s contained in this value.
pub fn as_slice(&self) -> &[TStringPart] {
match &self.inner {
TStringValueInner::Single(part) => std::slice::from_ref(part),
TStringValueInner::Concatenated(parts) => parts,
}
}
/// Returns a mutable slice of all the [`TStringPart`]s contained in this value.
fn as_mut_slice(&mut self) -> &mut [TStringPart] {
match &mut self.inner {
TStringValueInner::Single(part) => std::slice::from_mut(part),
TStringValueInner::Concatenated(parts) => parts,
}
}
/// Returns an iterator over all the [`TStringPart`]s contained in this value.
pub fn iter(&self) -> Iter<TStringPart> {
self.as_slice().iter()
}
/// Returns an iterator over all the [`TStringPart`]s contained in this value
/// that allows modification.
pub fn iter_mut(&mut self) -> IterMut<TStringPart> {
self.as_mut_slice().iter_mut()
}
/// Returns an iterator over the [`StringLiteral`] parts contained in this value.
///
/// Note that this doesn't recurse into the t-string parts. For example,
///
/// ```python
/// "foo" t"bar {x}" "baz" t"qux"
/// ```
///
/// Here, the string literal parts returned would be `"foo"` and `"baz"`.
pub fn literals(&self) -> impl Iterator<Item = &StringLiteral> {
self.iter().filter_map(|part| part.as_literal())
}
/// Returns an iterator over the [`TString`] parts contained in this value.
///
/// Note that this doesn't recurse into the t-string parts. For example,
///
/// ```python
/// "foo" t"bar {x}" "baz" t"qux"
/// ```
///
/// Here, the t-string parts returned would be `f"bar {x}"` and `f"qux"`.
pub fn t_strings(&self) -> impl Iterator<Item = &TString> {
self.iter().filter_map(|part| part.as_t_string())
}
/// Returns an iterator over all the [`InterpolatedStringElement`] contained in this value.
///
/// An t-string element is what makes up an [`TString`] i.e., it is either a
/// string literal or an interpolation. In the following example,
///
/// ```python
/// "foo" t"bar {x}" "baz" t"qux"
/// ```
///
/// The t-string elements returned would be string literal (`"bar "`),
/// interpolation (`x`) and string literal (`"qux"`).
pub fn elements(&self) -> impl Iterator<Item = &InterpolatedStringElement> {
self.t_strings().flat_map(|fstring| fstring.elements.iter())
}
}
impl<'a> IntoIterator for &'a TStringValue {
type Item = &'a TStringPart;
type IntoIter = Iter<'a, TStringPart>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl<'a> IntoIterator for &'a mut TStringValue {
type Item = &'a mut TStringPart;
type IntoIter = IterMut<'a, TStringPart>;
fn into_iter(self) -> Self::IntoIter {
self.iter_mut()
}
}
/// An internal representation of [`TStringValue`].
#[derive(Clone, Debug, PartialEq)]
enum TStringValueInner {
/// A single t-string i.e., `t"foo"`.
///
/// This is always going to be `TStringPart::TString` variant which is
/// maintained by the `TStringValue::single` constructor.
Single(TStringPart),
/// An implicitly concatenated t-string i.e., `"foo" t"bar {x}"`.
Concatenated(Vec<TStringPart>),
}
/// An t-string part which is either a string literal, an f-string,
/// or a t-string.
#[derive(Clone, Debug, PartialEq, is_macro::Is)]
pub enum TStringPart {
Literal(StringLiteral),
FString(FString),
TString(TString),
}
impl TStringPart {
pub fn quote_style(&self) -> Quote {
match self {
Self::Literal(string_literal) => string_literal.flags.quote_style(),
Self::FString(f_string) => f_string.flags.quote_style(),
Self::TString(t_string) => t_string.flags.quote_style(),
}
}
}
impl Ranged for TStringPart {
fn range(&self) -> TextRange {
match self {
TStringPart::Literal(string_literal) => string_literal.range(),
TStringPart::FString(f_string) => f_string.range(),
TStringPart::TString(t_string) => t_string.range(),
}
}
}
pub trait StringFlags: Copy {
/// Does the string use single or double quotes in its opener and closer?
fn quote_style(self) -> Quote;
@ -635,7 +812,7 @@ impl std::fmt::Display for DisplayFlags<'_> {
bitflags! {
#[derive(Default, Copy, Clone, PartialEq, Eq, Hash)]
struct FStringFlagsInner: u8 {
struct InterpolatedStringFlagsInner: u8 {
/// The f-string uses double quotes (`"`) for its opener and closer.
/// If this flag is not set, the f-string uses single quotes (`'`)
/// for its opener and closer.
@ -662,6 +839,11 @@ bitflags! {
/// Flags that can be queried to obtain information
/// regarding the prefixes and quotes used for an f-string.
///
/// Note: This is identical to [`TStringFlags`] except that
/// the implementation of the `prefix` method of the
/// [`StringFlags`] trait returns a variant of
/// `AnyStringPrefix::Format`.
///
/// ## Notes on usage
///
/// If you're using a `Generator` from the `ruff_python_codegen` crate to generate a lint-rule fix
@ -671,7 +853,7 @@ bitflags! {
/// will properly handle nested f-strings. For usage that doesn't fit into one of these categories,
/// the public constructor [`FStringFlags::empty`] can be used.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct FStringFlags(FStringFlagsInner);
pub struct FStringFlags(InterpolatedStringFlagsInner);
impl FStringFlags {
/// Construct a new [`FStringFlags`] with **no flags set**.
@ -684,42 +866,60 @@ impl FStringFlags {
/// situations in which alternative ways to construct this struct should be used, especially
/// when writing lint rules.
pub fn empty() -> Self {
Self(FStringFlagsInner::empty())
Self(InterpolatedStringFlagsInner::empty())
}
#[must_use]
pub fn with_quote_style(mut self, quote_style: Quote) -> Self {
self.0
.set(FStringFlagsInner::DOUBLE, quote_style.is_double());
self.0.set(
InterpolatedStringFlagsInner::DOUBLE,
quote_style.is_double(),
);
self
}
#[must_use]
pub fn with_triple_quotes(mut self, triple_quotes: TripleQuotes) -> Self {
self.0
.set(FStringFlagsInner::TRIPLE_QUOTED, triple_quotes.is_yes());
self.0.set(
InterpolatedStringFlagsInner::TRIPLE_QUOTED,
triple_quotes.is_yes(),
);
self
}
#[must_use]
pub fn with_prefix(mut self, prefix: FStringPrefix) -> Self {
match prefix {
FStringPrefix::Regular => {
Self(self.0 - FStringFlagsInner::R_PREFIX_LOWER - FStringFlagsInner::R_PREFIX_UPPER)
}
FStringPrefix::Regular => Self(
self.0
- InterpolatedStringFlagsInner::R_PREFIX_LOWER
- InterpolatedStringFlagsInner::R_PREFIX_UPPER,
),
FStringPrefix::Raw { uppercase_r } => {
self.0.set(FStringFlagsInner::R_PREFIX_UPPER, uppercase_r);
self.0.set(FStringFlagsInner::R_PREFIX_LOWER, !uppercase_r);
self.0
.set(InterpolatedStringFlagsInner::R_PREFIX_UPPER, uppercase_r);
self.0
.set(InterpolatedStringFlagsInner::R_PREFIX_LOWER, !uppercase_r);
self
}
}
}
pub const fn prefix(self) -> FStringPrefix {
if self.0.contains(FStringFlagsInner::R_PREFIX_LOWER) {
debug_assert!(!self.0.contains(FStringFlagsInner::R_PREFIX_UPPER));
if self
.0
.contains(InterpolatedStringFlagsInner::R_PREFIX_LOWER)
{
debug_assert!(
!self
.0
.contains(InterpolatedStringFlagsInner::R_PREFIX_UPPER)
);
FStringPrefix::Raw { uppercase_r: false }
} else if self.0.contains(FStringFlagsInner::R_PREFIX_UPPER) {
} else if self
.0
.contains(InterpolatedStringFlagsInner::R_PREFIX_UPPER)
{
FStringPrefix::Raw { uppercase_r: true }
} else {
FStringPrefix::Regular
@ -727,12 +927,108 @@ impl FStringFlags {
}
}
// TODO(dylan): the documentation about using
// `Checker::default_tstring_flags` is not yet
// correct. This method does not yet exist because
// introducing it would emit a dead code warning
// until we call it in lint rules.
/// Flags that can be queried to obtain information
/// regarding the prefixes and quotes used for an f-string.
///
/// Note: This is identical to [`FStringFlags`] except that
/// the implementation of the `prefix` method of the
/// [`StringFlags`] trait returns a variant of
/// `AnyStringPrefix::Template`.
///
/// ## Notes on usage
///
/// If you're using a `Generator` from the `ruff_python_codegen` crate to generate a lint-rule fix
/// from an existing t-string literal, consider passing along the [`FString::flags`] field. If you
/// don't have an existing literal but have a `Checker` from the `ruff_linter` crate available,
/// consider using `Checker::default_tstring_flags` to create instances of this struct; this method
/// will properly handle nested t-strings. For usage that doesn't fit into one of these categories,
/// the public constructor [`TStringFlags::empty`] can be used.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct TStringFlags(InterpolatedStringFlagsInner);
impl TStringFlags {
/// Construct a new [`TStringFlags`] with **no flags set**.
///
/// See [`TStringFlags::with_quote_style`], [`TStringFlags::with_triple_quotes`], and
/// [`TStringFlags::with_prefix`] for ways of setting the quote style (single or double),
/// enabling triple quotes, and adding prefixes (such as `r`), respectively.
///
/// See the documentation for [`TStringFlags`] for additional caveats on this constructor, and
/// situations in which alternative ways to construct this struct should be used, especially
/// when writing lint rules.
pub fn empty() -> Self {
Self(InterpolatedStringFlagsInner::empty())
}
#[must_use]
pub fn with_quote_style(mut self, quote_style: Quote) -> Self {
self.0.set(
InterpolatedStringFlagsInner::DOUBLE,
quote_style.is_double(),
);
self
}
#[must_use]
pub fn with_triple_quotes(mut self, triple_quotes: TripleQuotes) -> Self {
self.0.set(
InterpolatedStringFlagsInner::TRIPLE_QUOTED,
triple_quotes.is_yes(),
);
self
}
#[must_use]
pub fn with_prefix(mut self, prefix: TStringPrefix) -> Self {
match prefix {
TStringPrefix::Regular => Self(
self.0
- InterpolatedStringFlagsInner::R_PREFIX_LOWER
- InterpolatedStringFlagsInner::R_PREFIX_UPPER,
),
TStringPrefix::Raw { uppercase_r } => {
self.0
.set(InterpolatedStringFlagsInner::R_PREFIX_UPPER, uppercase_r);
self.0
.set(InterpolatedStringFlagsInner::R_PREFIX_LOWER, !uppercase_r);
self
}
}
}
pub const fn prefix(self) -> TStringPrefix {
if self
.0
.contains(InterpolatedStringFlagsInner::R_PREFIX_LOWER)
{
debug_assert!(
!self
.0
.contains(InterpolatedStringFlagsInner::R_PREFIX_UPPER)
);
TStringPrefix::Raw { uppercase_r: false }
} else if self
.0
.contains(InterpolatedStringFlagsInner::R_PREFIX_UPPER)
{
TStringPrefix::Raw { uppercase_r: true }
} else {
TStringPrefix::Regular
}
}
}
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 triple_quotes(self) -> TripleQuotes {
if self.0.contains(FStringFlagsInner::TRIPLE_QUOTED) {
if self.0.contains(InterpolatedStringFlagsInner::TRIPLE_QUOTED) {
TripleQuotes::Yes
} else {
TripleQuotes::No
@ -744,7 +1040,7 @@ impl StringFlags for FStringFlags {
/// - `f"{"a"}"` -> `QuoteStyle::Double`
/// - `f'{"a"}'` -> `QuoteStyle::Single`
fn quote_style(self) -> Quote {
if self.0.contains(FStringFlagsInner::DOUBLE) {
if self.0.contains(InterpolatedStringFlagsInner::DOUBLE) {
Quote::Double
} else {
Quote::Single
@ -766,11 +1062,50 @@ impl fmt::Debug for FStringFlags {
}
}
impl StringFlags for TStringFlags {
/// Return `true` if the t-string is triple-quoted, i.e.,
/// it begins and ends with three consecutive quote characters.
/// For example: `t"""{bar}"""`
fn triple_quotes(self) -> TripleQuotes {
if self.0.contains(InterpolatedStringFlagsInner::TRIPLE_QUOTED) {
TripleQuotes::Yes
} else {
TripleQuotes::No
}
}
/// Return the quoting style (single or double quotes)
/// used by the t-string's opener and closer:
/// - `t"{"a"}"` -> `QuoteStyle::Double`
/// - `t'{"a"}'` -> `QuoteStyle::Single`
fn quote_style(self) -> Quote {
if self.0.contains(InterpolatedStringFlagsInner::DOUBLE) {
Quote::Double
} else {
Quote::Single
}
}
fn prefix(self) -> AnyStringPrefix {
AnyStringPrefix::Template(self.prefix())
}
}
impl fmt::Debug for TStringFlags {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TStringFlags")
.field("quote_style", &self.quote_style())
.field("prefix", &self.prefix())
.field("triple_quoted", &self.is_triple_quoted())
.finish()
}
}
/// An AST node that represents a single f-string which is part of an [`ExprFString`].
#[derive(Clone, Debug, PartialEq)]
pub struct FString {
pub range: TextRange,
pub elements: FStringElements,
pub elements: InterpolatedStringElements,
pub flags: FStringFlags,
}
@ -784,66 +1119,84 @@ impl From<FString> for Expr {
}
}
/// A newtype wrapper around a list of [`FStringElement`].
/// A newtype wrapper around a list of [`InterpolatedStringElement`].
#[derive(Clone, Default, PartialEq)]
pub struct FStringElements(Vec<FStringElement>);
pub struct InterpolatedStringElements(Vec<InterpolatedStringElement>);
impl FStringElements {
/// Returns an iterator over all the [`FStringLiteralElement`] nodes contained in this f-string.
pub fn literals(&self) -> impl Iterator<Item = &FStringLiteralElement> {
impl InterpolatedStringElements {
/// Returns an iterator over all the [`InterpolatedStringLiteralElement`] nodes contained in this f-string.
pub fn literals(&self) -> impl Iterator<Item = &InterpolatedStringLiteralElement> {
self.iter().filter_map(|element| element.as_literal())
}
/// Returns an iterator over all the [`FStringExpressionElement`] nodes contained in this f-string.
pub fn expressions(&self) -> impl Iterator<Item = &FStringExpressionElement> {
self.iter().filter_map(|element| element.as_expression())
/// Returns an iterator over all the [`InterpolatedElement`] nodes contained in this f-string.
pub fn interpolations(&self) -> impl Iterator<Item = &InterpolatedElement> {
self.iter().filter_map(|element| element.as_interpolation())
}
}
impl From<Vec<FStringElement>> for FStringElements {
fn from(elements: Vec<FStringElement>) -> Self {
FStringElements(elements)
impl From<Vec<InterpolatedStringElement>> for InterpolatedStringElements {
fn from(elements: Vec<InterpolatedStringElement>) -> Self {
InterpolatedStringElements(elements)
}
}
impl<'a> IntoIterator for &'a FStringElements {
type IntoIter = Iter<'a, FStringElement>;
type Item = &'a FStringElement;
impl<'a> IntoIterator for &'a InterpolatedStringElements {
type IntoIter = Iter<'a, InterpolatedStringElement>;
type Item = &'a InterpolatedStringElement;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl<'a> IntoIterator for &'a mut FStringElements {
type IntoIter = IterMut<'a, FStringElement>;
type Item = &'a mut FStringElement;
impl<'a> IntoIterator for &'a mut InterpolatedStringElements {
type IntoIter = IterMut<'a, InterpolatedStringElement>;
type Item = &'a mut InterpolatedStringElement;
fn into_iter(self) -> Self::IntoIter {
self.iter_mut()
}
}
impl Deref for FStringElements {
type Target = [FStringElement];
impl Deref for InterpolatedStringElements {
type Target = [InterpolatedStringElement];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for FStringElements {
impl DerefMut for InterpolatedStringElements {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl fmt::Debug for FStringElements {
impl fmt::Debug for InterpolatedStringElements {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}
/// An AST node that represents a single t-string which is part of an [`ExprTString`].
#[derive(Clone, Debug, PartialEq)]
pub struct TString {
pub range: TextRange,
pub elements: InterpolatedStringElements,
pub flags: TStringFlags,
}
impl From<TString> for Expr {
fn from(payload: TString) -> Self {
ExprTString {
range: payload.range,
value: TStringValue::single(payload),
}
.into()
}
}
impl ExprStringLiteral {
/// Return `Some(literal)` if the string only consists of a single `StringLiteral` part
/// (indicating that it is not implicitly concatenated). Otherwise, return `None`.
@ -1662,18 +2015,23 @@ bitflags! {
/// but can have no other prefixes.
const F_PREFIX = 1 << 4;
/// The string has a `t` or `T` prefix, meaning it is a t-string.
/// T-strings can also be raw strings,
/// but can have no other prefixes.
const T_PREFIX = 1 << 5;
/// The string has an `r` prefix, meaning it is a raw string.
/// F-strings and byte-strings can be raw,
/// as can strings with no other prefixes.
/// U-strings cannot be raw.
const R_PREFIX_LOWER = 1 << 5;
const R_PREFIX_LOWER = 1 << 6;
/// The string has an `R` prefix, meaning it is a raw string.
/// The casing of the `r`/`R` has no semantic significance at runtime;
/// see https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#r-strings-and-r-strings
/// for why we track the casing of the `r` prefix,
/// but not for any other prefix
const R_PREFIX_UPPER = 1 << 6;
const R_PREFIX_UPPER = 1 << 7;
}
}
@ -1711,6 +2069,15 @@ impl AnyStringFlags {
AnyStringPrefix::Format(FStringPrefix::Raw { uppercase_r: true }) => {
AnyStringFlagsInner::F_PREFIX.union(AnyStringFlagsInner::R_PREFIX_UPPER)
}
// t-strings
AnyStringPrefix::Template(TStringPrefix::Regular) => AnyStringFlagsInner::T_PREFIX,
AnyStringPrefix::Template(TStringPrefix::Raw { uppercase_r: false }) => {
AnyStringFlagsInner::T_PREFIX.union(AnyStringFlagsInner::R_PREFIX_LOWER)
}
AnyStringPrefix::Template(TStringPrefix::Raw { uppercase_r: true }) => {
AnyStringFlagsInner::T_PREFIX.union(AnyStringFlagsInner::R_PREFIX_UPPER)
}
};
self
}
@ -1734,9 +2101,10 @@ impl AnyStringFlags {
)
}
/// Does the string have an `f` or `F` prefix?
pub const fn is_f_string(self) -> bool {
self.0.contains(AnyStringFlagsInner::F_PREFIX)
/// Does the string have an `f`,`F`,`t`, or `T` prefix?
pub const fn is_interpolated_string(self) -> bool {
self.0
.intersects(AnyStringFlagsInner::F_PREFIX.union(AnyStringFlagsInner::T_PREFIX))
}
/// Does the string have a `b` or `B` prefix?
@ -1793,6 +2161,17 @@ impl StringFlags for AnyStringFlags {
return AnyStringPrefix::Format(FStringPrefix::Regular);
}
// t-strings
if flags.contains(AnyStringFlagsInner::T_PREFIX) {
if flags.contains(AnyStringFlagsInner::R_PREFIX_LOWER) {
return AnyStringPrefix::Template(TStringPrefix::Raw { uppercase_r: false });
}
if flags.contains(AnyStringFlagsInner::R_PREFIX_UPPER) {
return AnyStringPrefix::Template(TStringPrefix::Raw { uppercase_r: true });
}
return AnyStringPrefix::Template(TStringPrefix::Regular);
}
// bytestrings
if flags.contains(AnyStringFlagsInner::B_PREFIX) {
if flags.contains(AnyStringFlagsInner::R_PREFIX_LOWER) {
@ -1872,7 +2251,7 @@ impl From<BytesLiteralFlags> for AnyStringFlags {
impl From<AnyStringFlags> for FStringFlags {
fn from(value: AnyStringFlags) -> FStringFlags {
let AnyStringPrefix::Format(fstring_prefix) = value.prefix() else {
let AnyStringPrefix::Format(prefix) = value.prefix() else {
unreachable!(
"Should never attempt to convert {} into an f-string",
value.prefix()
@ -1880,7 +2259,7 @@ impl From<AnyStringFlags> for FStringFlags {
};
FStringFlags::empty()
.with_quote_style(value.quote_style())
.with_prefix(fstring_prefix)
.with_prefix(prefix)
.with_triple_quotes(value.triple_quotes())
}
}
@ -1891,6 +2270,27 @@ impl From<FStringFlags> for AnyStringFlags {
}
}
impl From<AnyStringFlags> for TStringFlags {
fn from(value: AnyStringFlags) -> TStringFlags {
let AnyStringPrefix::Template(prefix) = value.prefix() else {
unreachable!(
"Should never attempt to convert {} into a t-string",
value.prefix()
)
};
TStringFlags::empty()
.with_quote_style(value.quote_style())
.with_prefix(prefix)
.with_triple_quotes(value.triple_quotes())
}
}
impl From<TStringFlags> for AnyStringFlags {
fn from(value: TStringFlags) -> Self {
value.as_any_string_flags()
}
}
#[derive(Clone, Debug, PartialEq, is_macro::Is)]
pub enum Number {
Int(int::Int),

View file

@ -72,7 +72,8 @@ impl OperatorPrecedence {
| ExprRef::BooleanLiteral(_)
| ExprRef::NoneLiteral(_)
| ExprRef::EllipsisLiteral(_)
| ExprRef::FString(_) => Self::Atomic,
| ExprRef::FString(_)
| ExprRef::TString(_) => Self::Atomic,
// Subscription, slicing, call, attribute reference
ExprRef::Attribute(_)
| ExprRef::Subscript(_)

View file

@ -59,6 +59,13 @@ impl PythonVersion {
Self::PY313
}
/// The latest Python version supported in preview
pub fn latest_preview() -> Self {
let latest_preview = Self::PY314;
debug_assert!(latest_preview >= Self::latest());
latest_preview
}
pub const fn latest_ty() -> Self {
// Make sure to update the default value for `EnvironmentOptions::python_version` when bumping this version.
Self::PY313

View file

@ -72,6 +72,9 @@ impl Transformer for Relocator {
Expr::FString(ast::ExprFString { range, .. }) => {
*range = self.range;
}
Expr::TString(ast::ExprTString { range, .. }) => {
*range = self.range;
}
Expr::StringLiteral(ast::ExprStringLiteral { range, .. }) => {
*range = self.range;
}

View file

@ -5,7 +5,7 @@ use std::sync::LazyLock;
use ruff_text_size::{TextLen, TextRange};
/// Enumeration of the two kinds of quotes that can be used
/// for Python string/f-string/bytestring literals
/// for Python string/f/t-string/bytestring literals
#[derive(Debug, Default, Copy, Clone, Hash, PartialEq, Eq, is_macro::Is)]
pub enum Quote {
/// E.g. `'`

View file

@ -91,6 +91,47 @@ impl fmt::Display for FStringPrefix {
}
}
/// Enumeration of the valid prefixes a t-string literal can have.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum TStringPrefix {
/// Just a regular t-string with no other prefixes, e.g. t"{bar}"
Regular,
/// A "raw" template string, that has an `r` or `R` prefix,
/// e.g. `rt"{bar}"` or `Rt"{bar}"`
Raw { uppercase_r: bool },
}
impl TStringPrefix {
/// Return a `str` representation of the prefix
pub const fn as_str(self) -> &'static str {
match self {
Self::Regular => "t",
Self::Raw { uppercase_r: true } => "Rt",
Self::Raw { uppercase_r: false } => "rt",
}
}
pub const fn text_len(self) -> TextSize {
match self {
Self::Regular => TextSize::new(1),
Self::Raw { .. } => TextSize::new(2),
}
}
/// Return true if this prefix indicates a "raw t-string",
/// e.g. `rt"{bar}"` or `Rt"{bar}"`
pub const fn is_raw(self) -> bool {
matches!(self, Self::Raw { .. })
}
}
impl fmt::Display for TStringPrefix {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
/// Enumeration of the valid prefixes a bytestring literal can have.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum ByteStringPrefix {
@ -151,6 +192,9 @@ pub enum AnyStringPrefix {
/// Prefixes that indicate the string is an f-string
Format(FStringPrefix),
/// Prefixes that indicate the string is a t-string
Template(TStringPrefix),
/// All other prefixes
Regular(StringLiteralPrefix),
}
@ -161,6 +205,7 @@ impl AnyStringPrefix {
Self::Regular(regular_prefix) => regular_prefix.as_str(),
Self::Bytes(bytestring_prefix) => bytestring_prefix.as_str(),
Self::Format(fstring_prefix) => fstring_prefix.as_str(),
Self::Template(tstring_prefix) => tstring_prefix.as_str(),
}
}
@ -169,6 +214,7 @@ impl AnyStringPrefix {
Self::Regular(regular_prefix) => regular_prefix.text_len(),
Self::Bytes(bytestring_prefix) => bytestring_prefix.text_len(),
Self::Format(fstring_prefix) => fstring_prefix.text_len(),
Self::Template(tstring_prefix) => tstring_prefix.text_len(),
}
}
@ -177,6 +223,7 @@ impl AnyStringPrefix {
Self::Regular(regular_prefix) => regular_prefix.is_raw(),
Self::Bytes(bytestring_prefix) => bytestring_prefix.is_raw(),
Self::Format(fstring_prefix) => fstring_prefix.is_raw(),
Self::Template(tstring_prefix) => tstring_prefix.is_raw(),
}
}
}

View file

@ -5,10 +5,10 @@ pub mod transformer;
use crate::{
self as ast, Alias, AnyParameterRef, Arguments, BoolOp, BytesLiteral, CmpOp, Comprehension,
Decorator, ElifElseClause, ExceptHandler, Expr, ExprContext, FString, FStringElement,
FStringPart, Keyword, MatchCase, Operator, Parameter, Parameters, Pattern, PatternArguments,
PatternKeyword, Stmt, StringLiteral, TypeParam, TypeParamParamSpec, TypeParamTypeVar,
TypeParamTypeVarTuple, TypeParams, UnaryOp, WithItem,
Decorator, ElifElseClause, ExceptHandler, Expr, ExprContext, FString, FStringPart,
InterpolatedStringElement, Keyword, MatchCase, Operator, Parameter, Parameters, Pattern,
PatternArguments, PatternKeyword, Stmt, StringLiteral, TString, TStringPart, TypeParam,
TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, TypeParams, UnaryOp, WithItem,
};
/// A trait for AST visitors. Visits all nodes in the AST recursively in evaluation-order.
@ -99,8 +99,14 @@ pub trait Visitor<'a> {
fn visit_f_string(&mut self, f_string: &'a FString) {
walk_f_string(self, f_string);
}
fn visit_f_string_element(&mut self, f_string_element: &'a FStringElement) {
walk_f_string_element(self, f_string_element);
fn visit_interpolated_string_element(
&mut self,
interpolated_string_element: &'a InterpolatedStringElement,
) {
walk_interpolated_string_element(self, interpolated_string_element);
}
fn visit_t_string(&mut self, t_string: &'a TString) {
walk_t_string(self, t_string);
}
fn visit_string_literal(&mut self, string_literal: &'a StringLiteral) {
walk_string_literal(self, string_literal);
@ -484,6 +490,17 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) {
}
}
}
Expr::TString(ast::ExprTString { value, .. }) => {
for part in value {
match part {
TStringPart::Literal(string_literal) => {
visitor.visit_string_literal(string_literal);
}
TStringPart::FString(f_string) => visitor.visit_f_string(f_string),
TStringPart::TString(t_string) => visitor.visit_t_string(t_string),
}
}
}
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
for string_literal in value {
visitor.visit_string_literal(string_literal);
@ -739,30 +756,36 @@ pub fn walk_pattern_keyword<'a, V: Visitor<'a> + ?Sized>(
}
pub fn walk_f_string<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, f_string: &'a FString) {
for f_string_element in &f_string.elements {
visitor.visit_f_string_element(f_string_element);
for interpolated_string_element in &f_string.elements {
visitor.visit_interpolated_string_element(interpolated_string_element);
}
}
pub fn walk_f_string_element<'a, V: Visitor<'a> + ?Sized>(
pub fn walk_interpolated_string_element<'a, V: Visitor<'a> + ?Sized>(
visitor: &mut V,
f_string_element: &'a FStringElement,
interpolated_string_element: &'a InterpolatedStringElement,
) {
if let ast::FStringElement::Expression(ast::FStringExpressionElement {
if let ast::InterpolatedStringElement::Interpolation(ast::InterpolatedElement {
expression,
format_spec,
..
}) = f_string_element
}) = interpolated_string_element
{
visitor.visit_expr(expression);
if let Some(format_spec) = format_spec {
for spec_element in &format_spec.elements {
visitor.visit_f_string_element(spec_element);
visitor.visit_interpolated_string_element(spec_element);
}
}
}
}
pub fn walk_t_string<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, t_string: &'a TString) {
for t_string_element in &t_string.elements {
visitor.visit_interpolated_string_element(t_string_element);
}
}
pub fn walk_expr_context<'a, V: Visitor<'a> + ?Sized>(
_visitor: &V,
_expr_context: &'a ExprContext,

View file

@ -1,8 +1,8 @@
use crate::{
Alias, Arguments, BoolOp, BytesLiteral, CmpOp, Comprehension, Decorator, ElifElseClause,
ExceptHandler, Expr, FString, FStringElement, Keyword, MatchCase, Mod, Operator, Parameter,
ParameterWithDefault, Parameters, Pattern, PatternArguments, PatternKeyword, Singleton, Stmt,
StringLiteral, TypeParam, TypeParams, UnaryOp, WithItem,
ExceptHandler, Expr, FString, InterpolatedStringElement, Keyword, MatchCase, Mod, Operator,
Parameter, ParameterWithDefault, Parameters, Pattern, PatternArguments, PatternKeyword,
Singleton, Stmt, StringLiteral, TString, TypeParam, TypeParams, UnaryOp, WithItem,
};
use crate::{AnyNodeRef, Identifier};
@ -157,8 +157,16 @@ pub trait SourceOrderVisitor<'a> {
}
#[inline]
fn visit_f_string_element(&mut self, f_string_element: &'a FStringElement) {
walk_f_string_element(self, f_string_element);
fn visit_interpolated_string_element(
&mut self,
interpolated_string_element: &'a InterpolatedStringElement,
) {
walk_interpolated_string_element(self, interpolated_string_element);
}
#[inline]
fn visit_t_string(&mut self, t_string: &'a TString) {
walk_t_string(self, t_string);
}
#[inline]
@ -272,6 +280,7 @@ where
Expr::Compare(expr) => expr.visit_source_order(visitor),
Expr::Call(expr) => expr.visit_source_order(visitor),
Expr::FString(expr) => expr.visit_source_order(visitor),
Expr::TString(expr) => expr.visit_source_order(visitor),
Expr::StringLiteral(expr) => expr.visit_source_order(visitor),
Expr::BytesLiteral(expr) => expr.visit_source_order(visitor),
Expr::NumberLiteral(expr) => expr.visit_source_order(visitor),
@ -497,15 +506,17 @@ where
visitor.leave_node(node);
}
pub fn walk_f_string_element<'a, V: SourceOrderVisitor<'a> + ?Sized>(
pub fn walk_interpolated_string_element<'a, V: SourceOrderVisitor<'a> + ?Sized>(
visitor: &mut V,
f_string_element: &'a FStringElement,
f_string_element: &'a InterpolatedStringElement,
) {
let node = AnyNodeRef::from(f_string_element);
if visitor.enter_node(node).is_traverse() {
match f_string_element {
FStringElement::Expression(element) => element.visit_source_order(visitor),
FStringElement::Literal(element) => element.visit_source_order(visitor),
InterpolatedStringElement::Interpolation(element) => {
element.visit_source_order(visitor);
}
InterpolatedStringElement::Literal(element) => element.visit_source_order(visitor),
}
}
visitor.leave_node(node);
@ -550,6 +561,18 @@ where
visitor.leave_node(node);
}
#[inline]
pub fn walk_t_string<'a, V>(visitor: &mut V, t_string: &'a TString)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
let node = AnyNodeRef::from(t_string);
if visitor.enter_node(node).is_traverse() {
t_string.visit_source_order(visitor);
}
visitor.leave_node(node);
}
#[inline]
pub fn walk_string_literal<'a, V>(visitor: &mut V, string_literal: &'a StringLiteral)
where

View file

@ -1,8 +1,8 @@
use crate::{
self as ast, Alias, Arguments, BoolOp, BytesLiteral, CmpOp, Comprehension, Decorator,
ElifElseClause, ExceptHandler, Expr, ExprContext, FString, FStringElement, Keyword, MatchCase,
Operator, Parameter, Parameters, Pattern, PatternArguments, PatternKeyword, Stmt,
StringLiteral, TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple,
ElifElseClause, ExceptHandler, Expr, ExprContext, FString, InterpolatedStringElement, Keyword,
MatchCase, Operator, Parameter, Parameters, Pattern, PatternArguments, PatternKeyword, Stmt,
StringLiteral, TString, TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple,
TypeParams, UnaryOp, WithItem,
};
@ -86,8 +86,14 @@ pub trait Transformer {
fn visit_f_string(&self, f_string: &mut FString) {
walk_f_string(self, f_string);
}
fn visit_f_string_element(&self, f_string_element: &mut FStringElement) {
walk_f_string_element(self, f_string_element);
fn visit_interpolated_string_element(
&self,
interpolated_string_element: &mut InterpolatedStringElement,
) {
walk_interpolated_string_element(self, interpolated_string_element);
}
fn visit_t_string(&self, t_string: &mut TString) {
walk_t_string(self, t_string);
}
fn visit_string_literal(&self, string_literal: &mut StringLiteral) {
walk_string_literal(self, string_literal);
@ -470,6 +476,21 @@ pub fn walk_expr<V: Transformer + ?Sized>(visitor: &V, expr: &mut Expr) {
}
}
}
Expr::TString(ast::ExprTString { value, .. }) => {
for t_string_part in value.iter_mut() {
match t_string_part {
ast::TStringPart::Literal(string_literal) => {
visitor.visit_string_literal(string_literal);
}
ast::TStringPart::FString(f_string) => {
visitor.visit_f_string(f_string);
}
ast::TStringPart::TString(t_string) => {
visitor.visit_t_string(t_string);
}
}
}
}
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
for string_literal in value.iter_mut() {
visitor.visit_string_literal(string_literal);
@ -744,29 +765,35 @@ pub fn walk_pattern_keyword<V: Transformer + ?Sized>(
pub fn walk_f_string<V: Transformer + ?Sized>(visitor: &V, f_string: &mut FString) {
for element in &mut f_string.elements {
visitor.visit_f_string_element(element);
visitor.visit_interpolated_string_element(element);
}
}
pub fn walk_f_string_element<V: Transformer + ?Sized>(
pub fn walk_interpolated_string_element<V: Transformer + ?Sized>(
visitor: &V,
f_string_element: &mut FStringElement,
interpolated_string_element: &mut InterpolatedStringElement,
) {
if let ast::FStringElement::Expression(ast::FStringExpressionElement {
if let ast::InterpolatedStringElement::Interpolation(ast::InterpolatedElement {
expression,
format_spec,
..
}) = f_string_element
}) = interpolated_string_element
{
visitor.visit_expr(expression);
if let Some(format_spec) = format_spec {
for spec_element in &mut format_spec.elements {
visitor.visit_f_string_element(spec_element);
visitor.visit_interpolated_string_element(spec_element);
}
}
}
}
pub fn walk_t_string<V: Transformer + ?Sized>(visitor: &V, t_string: &mut TString) {
for element in &mut t_string.elements {
visitor.visit_interpolated_string_element(element);
}
}
pub fn walk_expr_context<V: Transformer + ?Sized>(_visitor: &V, _expr_context: &mut ExprContext) {}
pub fn walk_bool_op<V: Transformer + ?Sized>(_visitor: &V, _bool_op: &mut BoolOp) {}

View file

@ -1,19 +1,36 @@
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_parser::{ParseError, parse_expression};
#[track_caller]
fn assert_comparable(left: &str, right: &str) -> Result<(), ParseError> {
let left_parsed = parse_expression(left)?;
let right_parsed = parse_expression(right)?;
let left_compr = ComparableExpr::from(left_parsed.expr());
let right_compr = ComparableExpr::from(right_parsed.expr());
assert_eq!(left_compr, right_compr);
Ok(())
}
#[track_caller]
fn assert_noncomparable(left: &str, right: &str) -> Result<(), ParseError> {
let left_parsed = parse_expression(left)?;
let right_parsed = parse_expression(right)?;
let left_compr = ComparableExpr::from(left_parsed.expr());
let right_compr = ComparableExpr::from(right_parsed.expr());
assert_ne!(left_compr, right_compr);
Ok(())
}
#[test]
fn concatenated_strings_compare_equal() -> Result<(), ParseError> {
let split_contents = r#"'a' 'b' r'\n raw'"#;
let value_contents = r#"'ab\\n raw'"#;
let split_parsed = parse_expression(split_contents)?;
let value_parsed = parse_expression(value_contents)?;
let split_compr = ComparableExpr::from(split_parsed.expr());
let value_compr = ComparableExpr::from(value_parsed.expr());
assert_eq!(split_compr, value_compr);
Ok(())
assert_comparable(split_contents, value_contents)
}
#[test]
@ -21,14 +38,7 @@ fn concatenated_bytes_compare_equal() -> Result<(), ParseError> {
let split_contents = r#"b'a' b'b'"#;
let value_contents = r#"b'ab'"#;
let split_parsed = parse_expression(split_contents)?;
let value_parsed = parse_expression(value_contents)?;
let split_compr = ComparableExpr::from(split_parsed.expr());
let value_compr = ComparableExpr::from(value_parsed.expr());
assert_eq!(split_compr, value_compr);
Ok(())
assert_comparable(split_contents, value_contents)
}
#[test]
@ -36,12 +46,45 @@ fn concatenated_fstrings_compare_equal() -> Result<(), ParseError> {
let split_contents = r#"f"{foo!r} this" r"\n raw" f" and {bar!s} that""#;
let value_contents = r#"f"{foo!r} this\\n raw and {bar!s} that""#;
let split_parsed = parse_expression(split_contents)?;
let value_parsed = parse_expression(value_contents)?;
let split_compr = ComparableExpr::from(split_parsed.expr());
let value_compr = ComparableExpr::from(value_parsed.expr());
assert_eq!(split_compr, value_compr);
Ok(())
assert_comparable(split_contents, value_contents)
}
#[test]
fn concatenated_tstrings_compare_equal() -> Result<(), ParseError> {
let split_contents = r#"t"{foo!r} this" r"\n raw" t" and {bar!s} that""#;
let value_contents = r#"t"{foo!r} this\\n raw and {bar!s} that""#;
assert_comparable(split_contents, value_contents)
}
#[test]
fn concatenated_f_and_t_strings_interwoven_compare_equal() -> Result<(), ParseError> {
let split_contents = r#"f"{foo} this " t"{bar}" "baz""#;
let value_contents = r#"f"{foo}" t" this {bar}" "baz""#;
assert_comparable(split_contents, value_contents)
}
#[test]
fn concatenated_f_and_t_strings_compare_unequal_when_swapped() -> Result<(), ParseError> {
let f_then_t_contents = r#"f"{foo!r} this" r"\n raw" t" and {bar!s} that""#;
let t_then_f_contents = r#"t"{foo!r} this" r"\n raw" f" and {bar!s} that""#;
assert_noncomparable(f_then_t_contents, t_then_f_contents)
}
#[test]
fn t_strings_literal_order_matters_compare_unequal() -> Result<(), ParseError> {
let interp_then_literal_contents = r#"t"{foo}bar""#;
let literal_then_interp_contents = r#"t"bar{foo}""#;
assert_noncomparable(interp_then_literal_contents, literal_then_interp_contents)
}
#[test]
fn t_strings_empty_concat_equal() -> Result<(), ParseError> {
let empty_literal = r#""" t"hey{foo}""#;
let empty_f_string = r#"f""t"hey{foo}""#;
assert_comparable(empty_literal, empty_f_string)
}

View file

@ -1,18 +1,17 @@
---
source: crates/ruff_python_ast_integration_tests/tests/source_order.rs
expression: trace
snapshot_kind: text
---
- ModModule
- StmtExpr
- ExprFString
- StringLiteral
- FString
- FStringLiteralElement
- FStringExpressionElement
- InterpolatedStringLiteralElement
- InterpolatedElement
- ExprName
- FStringLiteralElement
- FStringExpressionElement
- InterpolatedStringLiteralElement
- InterpolatedElement
- ExprName
- FStringLiteralElement
- FStringLiteralElement
- InterpolatedStringLiteralElement
- InterpolatedStringLiteralElement

View file

@ -0,0 +1,17 @@
---
source: crates/ruff_python_ast_integration_tests/tests/source_order.rs
expression: trace
---
- ModModule
- StmtExpr
- ExprTString
- StringLiteral
- TString
- InterpolatedStringLiteralElement
- InterpolatedElement
- ExprName
- InterpolatedStringLiteralElement
- InterpolatedElement
- ExprName
- InterpolatedStringLiteralElement
- InterpolatedStringLiteralElement

View file

@ -1,17 +1,16 @@
---
source: crates/ruff_python_ast_integration_tests/tests/visitor.rs
expression: trace
snapshot_kind: text
---
- StmtExpr
- ExprFString
- StringLiteral
- FString
- FStringLiteralElement
- FStringExpressionElement
- InterpolatedStringLiteralElement
- InterpolatedElement
- ExprName
- FStringLiteralElement
- FStringExpressionElement
- InterpolatedStringLiteralElement
- InterpolatedElement
- ExprName
- FStringLiteralElement
- FStringLiteralElement
- InterpolatedStringLiteralElement
- InterpolatedStringLiteralElement

View file

@ -0,0 +1,16 @@
---
source: crates/ruff_python_ast_integration_tests/tests/visitor.rs
expression: trace
---
- StmtExpr
- ExprTString
- StringLiteral
- TString
- InterpolatedStringLiteralElement
- InterpolatedElement
- ExprName
- InterpolatedStringLiteralElement
- InterpolatedElement
- ExprName
- InterpolatedStringLiteralElement
- InterpolatedStringLiteralElement

View file

@ -146,6 +146,15 @@ fn f_strings() {
assert_snapshot!(trace);
}
#[test]
fn t_strings() {
let source = r"'pre' t'foo {bar:.{x}f} baz'";
let trace = trace_source_order_visitation(source);
assert_snapshot!(trace);
}
fn trace_source_order_visitation(source: &str) -> String {
let parsed = parse(source, ParseOptions::from(Mode::Module)).unwrap();

View file

@ -4,13 +4,14 @@ use insta::assert_snapshot;
use ruff_python_ast::visitor::{
Visitor, walk_alias, walk_bytes_literal, walk_comprehension, walk_except_handler, walk_expr,
walk_f_string, walk_f_string_element, walk_keyword, walk_match_case, walk_parameter,
walk_parameters, walk_pattern, walk_stmt, walk_string_literal, walk_type_param, walk_with_item,
walk_f_string, walk_interpolated_string_element, walk_keyword, walk_match_case, walk_parameter,
walk_parameters, walk_pattern, walk_stmt, walk_string_literal, walk_t_string, walk_type_param,
walk_with_item,
};
use ruff_python_ast::{
self as ast, Alias, AnyNodeRef, BoolOp, BytesLiteral, CmpOp, Comprehension, ExceptHandler,
Expr, FString, FStringElement, Keyword, MatchCase, Operator, Parameter, Parameters, Pattern,
Stmt, StringLiteral, TypeParam, UnaryOp, WithItem,
Expr, FString, InterpolatedStringElement, Keyword, MatchCase, Operator, Parameter, Parameters,
Pattern, Stmt, StringLiteral, TString, TypeParam, UnaryOp, WithItem,
};
use ruff_python_parser::{Mode, ParseOptions, parse};
@ -154,6 +155,15 @@ fn f_strings() {
assert_snapshot!(trace);
}
#[test]
fn t_strings() {
let source = r"'pre' t'foo {bar:.{x}f} baz'";
let trace = trace_visitation(source);
assert_snapshot!(trace);
}
fn trace_visitation(source: &str) -> String {
let parsed = parse(source, ParseOptions::from(Mode::Module)).unwrap();
@ -318,9 +328,15 @@ impl Visitor<'_> for RecordVisitor {
self.exit_node();
}
fn visit_f_string_element(&mut self, f_string_element: &FStringElement) {
fn visit_interpolated_string_element(&mut self, f_string_element: &InterpolatedStringElement) {
self.enter_node(f_string_element);
walk_f_string_element(self, f_string_element);
walk_interpolated_string_element(self, f_string_element);
self.exit_node();
}
fn visit_t_string(&mut self, t_string: &TString) {
self.enter_node(t_string);
walk_t_string(self, t_string);
self.exit_node();
}
}

View file

@ -5,9 +5,9 @@ use std::ops::Deref;
use ruff_python_ast::{
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,
Comprehension, ConversionFlag, DebugText, ExceptHandler, Expr, 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};
@ -1112,6 +1112,9 @@ impl<'a> Generator<'a> {
Expr::FString(ast::ExprFString { value, .. }) => {
self.unparse_f_string_value(value);
}
Expr::TString(ast::ExprTString { value, .. }) => {
self.unparse_t_string_value(value);
}
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
self.unparse_string_literal_value(value);
}
@ -1326,24 +1329,24 @@ 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);
self.unparse_interpolated_string(&f_string.elements, f_string.flags.into());
}
}
}
}
fn unparse_f_string_body(&mut self, values: &[ast::FStringElement]) {
fn unparse_interpolated_string_body(&mut self, values: &[ast::InterpolatedStringElement]) {
for value in values {
self.unparse_f_string_element(value);
self.unparse_interpolated_string_element(value);
}
}
fn unparse_f_string_expression_element(
fn unparse_interpolated_element(
&mut self,
val: &Expr,
debug_text: Option<&DebugText>,
conversion: ConversionFlag,
spec: Option<&ast::FStringFormatSpec>,
spec: Option<&ast::InterpolatedStringFormatSpec>,
) {
let mut generator = Generator::new(self.indent, self.line_ending);
generator.unparse_expr(val, precedence::FORMATTED_VALUE);
@ -1379,18 +1382,21 @@ impl<'a> Generator<'a> {
self.p("}");
}
fn unparse_f_string_element(&mut self, element: &ast::FStringElement) {
fn unparse_interpolated_string_element(&mut self, element: &ast::InterpolatedStringElement) {
match element {
ast::FStringElement::Literal(ast::FStringLiteralElement { value, .. }) => {
self.unparse_f_string_literal_element(value);
ast::InterpolatedStringElement::Literal(ast::InterpolatedStringLiteralElement {
value,
..
}) => {
self.unparse_interpolated_string_literal_element(value);
}
ast::FStringElement::Expression(ast::FStringExpressionElement {
ast::InterpolatedStringElement::Interpolation(ast::InterpolatedElement {
expression,
debug_text,
conversion,
format_spec,
range: _,
}) => self.unparse_f_string_expression_element(
}) => self.unparse_interpolated_element(
expression,
debug_text.as_ref(),
*conversion,
@ -1399,24 +1405,46 @@ impl<'a> Generator<'a> {
}
}
fn unparse_f_string_literal_element(&mut self, s: &str) {
fn unparse_interpolated_string_literal_element(&mut self, s: &str) {
let s = s.replace('{', "{{").replace('}', "}}");
self.p(&s);
}
fn unparse_f_string_specifier(&mut self, values: &[ast::FStringElement]) {
self.unparse_f_string_body(values);
fn unparse_f_string_specifier(&mut self, values: &[ast::InterpolatedStringElement]) {
self.unparse_interpolated_string_body(values);
}
/// 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], flags: FStringFlags) {
fn unparse_interpolated_string(
&mut self,
values: &[ast::InterpolatedStringElement],
flags: AnyStringFlags,
) {
let mut generator = Generator::new(self.indent, self.line_ending);
generator.unparse_f_string_body(values);
generator.unparse_interpolated_string_body(values);
let body = &generator.buffer;
self.p_str_repr(body, flags);
}
fn unparse_t_string_value(&mut self, value: &ast::TStringValue) {
let mut first = true;
for t_string_part in value {
self.p_delim(&mut first, " ");
match t_string_part {
ast::TStringPart::Literal(string_literal) => {
self.unparse_string_literal(string_literal);
}
ast::TStringPart::FString(f_string) => {
self.unparse_interpolated_string(&f_string.elements, f_string.flags.into());
}
ast::TStringPart::TString(t_string) => {
self.unparse_interpolated_string(&t_string.elements, t_string.flags.into());
}
}
}
}
fn unparse_alias(&mut self, alias: &Alias) {
self.p_id(&alias.name);
if let Some(asname) = &alias.asname {

View file

@ -38,9 +38,9 @@ for node_line in node_lines:
# `FStringLiteralElement`, `FStringFormatSpec` and `FStringExpressionElement` are
# handled by the `FString` implementation.
if node in (
"FStringLiteralElement",
"FStringExpressionElement",
"FStringFormatSpec",
"InterpolatedStringLiteralElement",
"InterpolatedElement",
"InterpolatedStringFormatSpec",
"Identifier",
):
continue

View file

@ -431,3 +431,16 @@ f"hellooooooooooooooooooooooo \
worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb)
aaaaaaaaaaa = f"hellooooooooooooooooooooooo \
worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb)
# This t-string should be flattened
xxxxxxxxxxxxxxxx = t"aaaaaaaaaaaaaaaaaaaaa {
expression } bbbbbbbbbbbbbbbbbbbbbbbb" + (
yyyyyyyyyyyyyy + zzzzzzzzzzz
)
# This is not a multiline t-string, but the expression is too long so it should be
# wrapped in parentheses.
t"hellooooooooooooooooooooooo \
worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb)
aaaaaaaaaaa = t"hellooooooooooooooooooooooo \
worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb)

View file

@ -100,6 +100,55 @@ f"{10 + len('bar')=}" f'no debug{10}' f"{10 + len('bar')=}"
f"{10 + len('bar')=}" f'{10 + len("bar")=}'
##############################################################################
# T-strings
##############################################################################
# Escape `{` and `}` when merging a t-string with a string
"a {not_a_variable}" t"b {10}" "c"
# Join, and break expressions
t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{
expression
}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" t"cccccccccccccccccccc {20999}" "more"
# Join, but don't break the expressions
t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" t"cccccccccccccccccccc {20999}" "more"
t"test{
expression
}flat" t"can be {
joined
} together"
aaaaaaaaaaa = t"test{
expression
}flat" t"cean beeeeeeee {
joined
} eeeeeeeeeeeeeeeeeeeeeeeeeeeee" # inline
t"single quoted '{x}'" t'double quoted "{x}"' # Same number of quotes => use preferred quote style
t"single quote ' {x}" t'double quoted "{x}"' # More double quotes => use single quotes
t"single quoted '{x}'" t'double quote " {x}"' # More single quotes => use double quotes
# Different triple quoted strings
t"{'''test'''}" t'{"""other"""}'
# Now with inner quotes
t"{'''test ' '''}" t'{"""other " """}'
t"{some_where_nested('''test ' ''')}" t'{"""other " """ + "more"}'
t"{b'''test ' '''}" t'{b"""other " """}'
t"{t'''test ' '''}" t'{t"""other " """}'
# debug expressions containing quotes
t"{10 + len('bar')=}" t"{10 + len('bar')=}"
t"{10 + len('bar')=}" t'no debug{10}' t"{10 + len('bar')=}"
# We can't safely merge this pre Python 3.12 without altering the debug expression.
t"{10 + len('bar')=}" t'{10 + len("bar")=}'
##############################################################################
# Don't join raw strings
##############################################################################
@ -110,6 +159,9 @@ R"a" "normal"
f"test" fr"test"
f"test" fR"test"
t"test" tr"test"
t"test" tR"test"
##############################################################################
# Don't join triple quoted strings
@ -119,9 +171,22 @@ f"test" fR"test"
"single" f""""single"""
"single" t""""single"""
b"single" b"""triple"""
##############################################################################
# Don't join t-strings and f-strings
##############################################################################
t"{interp}" f"{expr}"
f"{expr}" t"{interp}"
f"{expr}" "string" t"{interp}"
##############################################################################
# Join strings in with statements
##############################################################################

View file

@ -293,6 +293,155 @@ aaaaa[aaaaaaaaaaa] = (
)
#############################################################
# T-Strings
#############################################################
# Flatten and join the t-string
aaaaaaaaaaa = t"test{
expression}flat" t"cean beeeeeeee {joined} eeeeeeeeeeeeeeeee" # inline
# Parenthesize the value and join it, inline the comment
aaaaaaaaaaa = t"test{
expression}flat" t"cean beeeeeeee {joined} eeeeeeeeeeeeeeeeeeeeeeeeeee" # inline
# Parenthesize the t-string and keep it multiline because it doesn't fit on a single line including the comment
aaaaaaaaaaa = t"test{
expression
}flat" t"cean beeeeeeee {
joined
} eeeeeeeeeeeeeeeeeeeeeeeeeeeee" # inline
# The target splits because of a magic trailing comma
# The string is joined and not parenthesized because it just fits into the line length (including comment).
a[
aaaaaaa,
b,
] = t"ccccc{
expression}ccccccccccc" t"cccccccccccccccccccccccccccccccccccccccccc" # comment
# Same but starting with a joined string. They should both result in the same formatting.
[
aaaaaaa,
b,
] = t"ccccc{
expression}ccccccccccccccccccccccccccccccccccccccccccccccccccccc" # comment
# The target splits because of the magic trailing comma
# The string is **not** joined because it with the inlined comment exceeds the line length limit.
a[
aaaaaaa,
b,
] = t"ccccc{
expression}cccccccccccccccccccc" t"cccccccccccccccccccccccccccccccccccccccccc" # comment
# The target should be flat
# The string should be joined because it fits into the line length
a[
aaaaaaa,
b
] = (
t"ccccc{
expression}ccccccccccc" "cccccccccccccccccccccccc" # comment
)
# Same but starting with a joined string. They should both result in the same formatting.
a[
aaaaaaa,
b
] = t"ccccc{
expression}ccccccccccccccccccccccccccccccccccc" # comment
# The target should be flat
# The string gets parenthesized because it, with the inlined comment, exceeds the line length limit.
a[
aaaaaaa,
b
] = t"ccccc{
expression}ccccccccccc" "ccccccccccccccccccccccccccccccccccccccccccc" # comment
# Split an overlong target, but join the string if it fits
a[
aaaaaaa,
b
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
t"ccccc{
expression}ccccccccccc" "cccccccccccccccccccccccccccccc" # comment
)
# Split both if necessary and keep multiline
a[
aaaaaaa,
b
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
t"ccccc{
expression}cccccccccccccccccccccccccccccccc" "ccccccccccccccccccccccccccccccc" # comment
)
# Don't inline t-strings that contain expressions that are guaranteed to split, e.b. because of a magic trailing comma
aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
)
aaaaa[aaaaaaaaaaa] = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
aaaaa[aaaaaaaaaaa] = (t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
)
# Don't inline t-strings that contain commented expressions
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{[
a # comment
]}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{[
a # comment
]}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
# Don't inline t-strings with multiline debug expressions:
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
a=}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a +
b=}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a
=}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
a=}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a
=}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
# Trailing last-part comments
a = (
@ -374,4 +523,4 @@ self._attr_unique_id = (
return (
f"Exception in {call_back_name} when handling msg on "
f"'{msg.topic}': '{msg.payload}'" # type: ignore[str-bytes-safe]
)
)

View file

@ -0,0 +1 @@
[{"target_version": "3.14"}]

View file

@ -0,0 +1,731 @@
(
t'{one}'
t'{two}'
)
rt"Not-so-tricky \"quote"
# Regression test for tstrings dropping comments
result_f = (
'Traceback (most recent call last):\n'
t' File "{__file__}", line {lineno_f+5}, in _check_recursive_traceback_display\n'
' f()\n'
t' File "{__file__}", line {lineno_f+1}, in f\n'
' f()\n'
t' File "{__file__}", line {lineno_f+1}, in f\n'
' f()\n'
t' File "{__file__}", line {lineno_f+1}, in f\n'
' f()\n'
# XXX: The following line changes depending on whether the tests
# are run through the interactive interpreter or with -m
# It also varies depending on the platform (stack size)
# Fortunately, we don't care about exactness here, so we use regex
r' \[Previous line repeated (\d+) more times\]' '\n'
'RecursionError: maximum recursion depth exceeded\n'
)
# Regression for tstring dropping comments that were accidentally attached to
# an expression inside a formatted value
(
t'{1}'
# comment 1
''
)
(
t'{1}' # comment 2
t'{2}'
)
(
t'{1}'
t'{2}' # comment 3
)
(
1, ( # comment 4
t'{2}'
)
)
(
(
t'{1}'
# comment 5
),
2
)
# https://github.com/astral-sh/ruff/issues/6841
x = t'''a{""}b'''
y = t'''c{1}d"""e'''
z = t'''a{""}b''' t'''c{1}d"""e'''
# T-String formatting test cases (Preview)
# Simple expression with a mix of debug expression and comments.
x = t"{a}"
x = t"{
a = }"
x = t"{ # comment 6
a }"
x = t"{ # comment 7
a = }"
# Remove the parentheses as adding them doesn't make then fit within the line length limit.
# This is similar to how we format it before t-string formatting.
aaaaaaaaaaa = (
t"asaaaaaaaaaaaaaaaa { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd } cccccccccc"
)
# Here, we would use the best fit layout to put the t-string indented on the next line
# similar to the next example.
aaaaaaaaaaa = t"asaaaaaaaaaaaaaaaa { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc } cccccccccc"
aaaaaaaaaaa = (
t"asaaaaaaaaaaaaaaaa { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc } cccccccccc"
)
# This should never add the optional parentheses because even after adding them, the
# t-string exceeds the line length limit.
x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc"
x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc"
x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 8
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc"
x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc"
x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbb' = } ccccccccccccccc"
# Multiple larger expressions which exceeds the line length limit. Here, we need to decide
# whether to split at the first or second expression. This should work similarly to the
# assignment statement formatting where we split from right to left in preview mode.
x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb } cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee"
# The above example won't split but when we start introducing line breaks:
x = t"aaaaaaaaaaaa {
bbbbbbbbbbbbbb } cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee"
x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb
} cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee"
x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb } cccccccccccccccccccc {
ddddddddddddddd } eeeeeeeeeeeeee"
x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb } cccccccccccccccccccc { ddddddddddddddd
} eeeeeeeeeeeeee"
# But, in case comments are present, we would split at the expression containing the
# comments:
x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb # comment 10
} cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee"
x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb
} cccccccccccccccccccc { # comment 11
ddddddddddddddd } eeeeeeeeeeeeee"
# Here, the expression part itself starts with a curly brace so we need to add an extra
# space between the opening curly brace and the expression.
x = t"{ {'x': 1, 'y': 2} }"
# Although the extra space isn't required before the ending curly brace, we add it for
# consistency.
x = t"{ {'x': 1, 'y': 2}}"
x = t"{ {'x': 1, 'y': 2} = }"
x = t"{ # comment 12
{'x': 1, 'y': 2} }"
x = t"{ # comment 13
{'x': 1, 'y': 2} = }"
# But, if there's a format specifier or a conversion flag then we don't need to add
# any whitespace at the end
x = t"aaaaa { {'aaaaaa', 'bbbbbbb', 'ccccccccc'}!s} bbbbbb"
x = t"aaaaa { {'aaaaaa', 'bbbbbbb', 'ccccccccc'}:.3f} bbbbbb"
# But, in this case, we would split the expression itself because it exceeds the line
# length limit so we need not add the extra space.
xxxxxxx = t"{
{'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbb', 'ccccccccccccccccccccc'}
}"
# And, split the expression itself because it exceeds the line length.
xxxxxxx = t"{
{'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'}
}"
#############################################################################################
# Quotes
#############################################################################################
t"foo 'bar' {x}"
t"foo \"bar\" {x}"
t'foo "bar" {x}'
t'foo \'bar\' {x}'
t"foo {"bar"}"
t"single quoted '{x}' double quoted \"{x}\"" # Same number of quotes => use preferred quote style
t"single quote ' {x} double quoted \"{x}\"" # More double quotes => use single quotes
t"single quoted '{x}' double quote \" {x}" # More single quotes => use double quotes
fr"single quotes ' {x}" # Keep double because `'` can't be escaped
fr'double quotes " {x}' # Keep single because `"` can't be escaped
fr'flip quotes {x}' # Use preferred quotes, because raw string contains now quotes.
# Here, the formatter will remove the escapes
t"foo {'\'bar\''}"
t"foo {'\"bar\"'}"
# Quotes inside the expressions have no impact on the quote selection of the outer string.
# Required so that the following two examples result in the same formatting.
t'foo {10 + len("bar")}'
t"foo {10 + len('bar')}"
# Pre 312, preserve the outer quotes if the t-string contains quotes in the debug expression
t'foo {10 + len("bar")=}'
t'''foo {10 + len('''bar''')=}'''
t'''foo {10 + len('bar')=}''' # Fine to change the quotes because it uses triple quotes
# Triple-quoted strings
# It's ok to use the same quote char for the inner string if it's single-quoted.
t"""test {'inner'}"""
t"""test {"inner"}"""
# But if the inner string is also triple-quoted then we should preserve the existing quotes.
t"""test {'''inner'''}"""
# It's not okay to change the quote style if the inner string is triple quoted and contains a quote.
t'{"""other " """}'
t'{"""other " """ + "more"}'
t'{b"""other " """}'
t'{t"""other " """}'
t"""test {t'inner {'''inner inner'''}'}"""
t"""test {t'''inner {"""inner inner"""}'''}"""
# Magic trailing comma
#
# The expression formatting will result in breaking it across multiple lines with a
# trailing comma but as the expression isn't already broken, we will remove all the line
# breaks which results in the trailing comma being present. This test case makes sure
# that the trailing comma is removed as well.
t"aaaaaaa {['aaaaaaaaaaaaaaa', 'bbbbbbbbbbbbb', 'ccccccccccccccccc', 'ddddddddddddddd', 'eeeeeeeeeeeeee']} aaaaaaa"
# And, if the trailing comma is already present, we still need to remove it.
t"aaaaaaa {['aaaaaaaaaaaaaaa', 'bbbbbbbbbbbbb', 'ccccccccccccccccc', 'ddddddddddddddd', 'eeeeeeeeeeeeee',]} aaaaaaa"
# Keep this Multiline by breaking it at the square brackets.
t"""aaaaaa {[
xxxxxxxx,
yyyyyyyy,
]} ccc"""
# Add the magic trailing comma because the elements don't fit within the line length limit
# when collapsed.
t"aaaaaa {[
xxxxxxxxxxxx,
xxxxxxxxxxxx,
xxxxxxxxxxxx,
xxxxxxxxxxxx,
xxxxxxxxxxxx,
xxxxxxxxxxxx,
yyyyyyyyyyyy
]} ccccccc"
# Remove the parentheses because they aren't required
xxxxxxxxxxxxxxx = (
t"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbb {
xxxxxxxxxxx # comment 14
+ yyyyyyyyyy
} dddddddddd"
)
# Comments
# No comments should be dropped!
t"{ # comment 15
# comment 16
foo # comment 17
# comment 18
}" # comment 19
# comment 20
# Single-quoted t-strings with a format specificer can be multiline
t"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {
variable:.3f} ddddddddddddddd eeeeeeee"
# But, if it's triple-quoted then we can't or the format specificer will have a
# trailing newline
t"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {
variable:.3f} ddddddddddddddd eeeeeeee"""
# But, we can break the ones which don't have a format specifier
t"""fooooooooooooooooooo barrrrrrrrrrrrrrrrrrr {
xxxxxxxxxxxxxxx:.3f} aaaaaaaaaaaaaaaaa { xxxxxxxxxxxxxxxxxxxx } bbbbbbbbbbbb"""
# Throw in a random comment in it but surprise, this is not a comment but just a text
# which is part of the format specifier
aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa {
aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f
# comment
} cccccccccc"""
aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa {
aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f
# comment} cccccccccc"""
# Conversion flags
#
# This is not a valid Python code because of the additional whitespace between the `!`
# and conversion type. But, our parser isn't strict about this. This should probably be
# removed once we have a strict parser.
x = t"aaaaaaaaa { x ! r }"
# Even in the case of debug expressions, we only need to preserve the whitespace within
# the expression part of the replacement field.
x = t"aaaaaaaaa { x = ! r }"
# Combine conversion flags with format specifiers
x = t"{x = ! s
:>0
}"
# This is interesting. There can be a comment after the format specifier but only if it's
# on it's own line. Refer to https://github.com/astral-sh/ruff/pull/7787 for more details.
# We'll format is as trailing comments.
x = t"{x !s
:>0
# comment 21
}"
x = t"""
{ # comment 22
x = :.0{y # comment 23
}f}"""
# Here, the debug expression is in a nested t-string so we should start preserving
# whitespaces from that point onwards. This means we should format the outer t-string.
x = t"""{"foo " + # comment 24
t"{ x =
}" # comment 25
}
"""
# Mix of various features.
t"{ # comment 26
foo # after foo
:>{
x # after x
}
# comment 27
# comment 28
} woah {x}"
# Assignment statement
# Even though this t-string has multiline expression, thus allowing us to break it at the
# curly braces, the t-string fits on a single line if it's moved inside the parentheses.
# We should prefer doing that instead.
aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
expression}moreeeeeeeeeeeeeeeee"
# Same as above
xxxxxxx = t"{
{'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'}
}"
# Similar to the previous example, but the t-string will exceed the line length limit,
# we shouldn't add any parentheses here.
xxxxxxx = t"{
{'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'}
}"
# Same as above but with an inline comment. The t-string should be formatted inside the
# parentheses and the comment should be part of the line inside the parentheses.
aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
expression}moreeeeeeeeeeeeeeeee" # comment
# Similar to the previous example but this time parenthesizing doesn't work because it
# exceeds the line length. So, avoid parenthesizing this t-string.
aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
expression}moreeeeeeeeeeeeeeeee" # comment loooooooong
# Similar to the previous example but we start with the parenthesized layout. This should
# remove the parentheses and format the t-string on a single line. This shows that the
# final layout for the formatter is same for this and the previous case. The only
# difference is that in the previous case the expression is already mulitline which means
# the formatter can break it further at the curly braces.
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" # comment loooooooong
)
# The following t-strings are going to break because of the trailing comma so we should
# avoid using the best fit layout and instead use the default layout.
# left-to-right
aaaa = t"aaaa {[
1, 2,
]} bbbb"
# right-to-left
aaaa, bbbb = t"aaaa {[
1, 2,
]} bbbb"
# Using the right-to-left assignment statement variant.
aaaaaaaaaaaaaaaaaa, bbbbbbbbbbb = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
expression}moreeeeeeeeeeeeeeeee" # comment
# Here, the t-string layout is flat but it exceeds the line length limit. This shouldn't
# try the custom best fit layout because the t-string doesn't have any split points.
aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = (
t"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd"
)
# Same as above but without the parentheses to test that it gets formatted to the same
# layout as the previous example.
aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = t"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd"
# But, the following t-string does have a split point because of the multiline expression.
aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = (
t"aaaaaaaaaaaaaaaaaaa {
aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd"
)
aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = (
t"aaaaaaaaaaaaaaaaaaa {
aaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbb + cccccccccccccccccccccc + dddddddddddddddddddddddddddd} ddddddddddddddddddd"
)
# This is an implicitly concatenated t-string but it cannot be joined because otherwise
# it'll exceed the line length limit. So, the two t-strings will be inside parentheses
# instead and the inline comment should be outside the parentheses.
a = t"test{
expression
}flat" t"can be {
joined
} togethereeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" # inline
# Similar to the above example but this fits within the line length limit.
a = t"test{
expression
}flat" t"can be {
joined
} togethereeeeeeeeeeeeeeeeeeeeeeeeeee" # inline
# The following test cases are adopted from implicit string concatenation but for a
# single t-string instead.
# Don't inline t-strings that contain expressions that are guaranteed to split, e.g. because of a magic trailing comma
aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}moreeeeeeeeeeeeeeeeeeee" # comment
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}moreeeeeeeeeeeeeeeeeeee" # comment
)
aaaaa[aaaaaaaaaaa] = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}moreeeeeeeeeeeeeeeeeeee" # comment
aaaaa[aaaaaaaaaaa] = (t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}moreeeeeeeeeeeeeeeeeeee" # comment
)
# Don't inline t-strings that contain commented expressions
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{[
a # comment
]}moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{[
a # comment
]}moreeeeeeeeeeeeeeeeeetest" # comment
)
# Don't inline t-strings with multiline debug expressions or format specifiers
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
a=}moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a +
b=}moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a
=}moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
a=}moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a
=}moreeeeeeeeeeeeeeeeeetest" # comment
)
# This is not a multiline t-string even though it has a newline after the format specifier.
aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeee{
a:.3f
}moreeeeeeeeeeeeeeeeeetest" # comment
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
a:.3f
}moreeeeeeeeeeeeeeeeeetest" # comment
)
# The newline is only considered when it's a tripled-quoted t-string.
aaaaaaaaaaaaaaaaaa = t"""testeeeeeeeeeeeeeeeeeeeeeeeee{
a:.3f
}moreeeeeeeeeeeeeeeeeetest""" # comment
aaaaaaaaaaaaaaaaaa = (
t"""testeeeeeeeeeeeeeeeeeeeeeeeee{
a:.3f
}moreeeeeeeeeeeeeeeeeetest""" # comment
)
# Remove the parentheses here
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{[a, b,
# comment
]}moee" # comment
)
# ... but not here because of the ownline comment
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{[a, b,
]}moee"
# comment
)
# t-strings in other positions
if t"aaaaaaaaaaa {ttttteeeeeeeeest} more {
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
}": pass
if (
t"aaaaaaaaaaa {ttttteeeeeeeeest} more {
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
}"
): pass
if t"aaaaaaaaaaa {ttttteeeeeeeeest} more {
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
}": pass
if t"aaaaaaaaaaa {ttttteeeeeeeeest} more { # comment
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
}": pass
if t"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more {
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
}":
pass
if (
t"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more {
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
}"
):
pass
if t"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more {
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
}":
pass
# For loops
for a in t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
expression}moreeeeeeeeeeeeeeeeeeee":
pass
for a in t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee":
pass
for a in t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee":
pass
for a in (
t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeee"
):
pass
# With statements
with t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
expression}moreeeeeeeeeeeeeeeeeeee":
pass
with t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee":
pass
with t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee":
pass
with (
t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeee"
):
pass
# Assert statements
assert t"aaaaaaaaa{
expression}bbbbbbbbbbbb", t"cccccccccc{
expression}dddddddddd"
assert t"aaaaaaaaa{expression}bbbbbbbbbbbb", t"cccccccccccccccc{
expression}dddddddddddddddd"
assert t"aaaaaaaaa{expression}bbbbbbbbbbbb", t"cccccccccccccccc{expression}dddddddddddddddd"
assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{
expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"ccccccc{expression}dddddddddd"
assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"ccccccc{expression}dddddddddd"
assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{
expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"ccccccccccccccccccccc {
expression} dddddddddddddddddddddddddd"
assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"cccccccccccccccccccccccccccccccc {expression} ddddddddddddddddddddddddddddddddddddd"
# t-strings as a single argument to a call expression to test whether it's huggable or not.
call(t"{
testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
}")
call(t"{
testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
}")
call(t"{ # comment
testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
}")
call(t"""aaaaaaaaaaaaaaaa bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee}""")
call(t"""aaaaaaaaaaaaaaaa bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
}""")
call(t"""aaaaaaaaaaaaaaaa
bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
}""")
call(t"""aaaaaaaaaaaaaaaa
bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment
}""")
call(
t"""aaaaaaaaaaaaaaaa
bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment
}"""
)
call(t"{
aaaaaa
+ '''test
more'''
}")
# Indentation
# What should be the indentation?
# https://github.com/astral-sh/ruff/discussions/9785#discussioncomment-8470590
if indent0:
if indent1:
if indent2:
foo = t"""hello world
hello {
t"aaaaaaa {
[
'aaaaaaaaaaaaaaaaaaaaa',
'bbbbbbbbbbbbbbbbbbbbb',
'ccccccccccccccccccccc',
'ddddddddddddddddddddd'
]
} bbbbbbbb" +
[
'aaaaaaaaaaaaaaaaaaaaa',
'bbbbbbbbbbbbbbbbbbbbb',
'ccccccccccccccccccccc',
'ddddddddddddddddddddd'
]
} --------
"""
# Implicit concatenated t-string containing quotes
_ = (
'This string should change its quotes to double quotes'
t'This string uses double quotes in an expression {"it's a quote"}'
t'This t-string does not use any quotes.'
)
# Regression test for https://github.com/astral-sh/ruff/issues/14487
t"aaaaaaaaaaaaaaaaaaaaaaaaaa {10**27} bbbbbbbbbbbbbbbbbbbbbbbbbb ccccccccccccccccccccccccc"
# Regression test for https://github.com/astral-sh/ruff/issues/14778
t"{'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 'a' if True else ""}"
t"{'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 'a' if True else ""}"
# Quotes reuse
t"{'a'}"
# 312+, it's okay to change the outer quotes even when there's a debug expression using the same quotes
t'foo {10 + len("bar")=}'
t'''foo {10 + len("""bar""")=}'''
# 312+, it's okay to change the quotes here without creating an invalid t-string
t'{"""other " """}'
t'{"""other " """ + "more"}'
t'{b"""other " """}'
t'{t"""other " """}'
# Regression tests for https://github.com/astral-sh/ruff/issues/13935
t'{1: hy "user"}'
t'{1:hy "user"}'
t'{1: abcd "{1}" }'
t'{1: abcd "{'aa'}" }'
t'{1=: "abcd {'aa'}}'
t'{x:a{z:hy "user"}} \'\'\''
# Changing the outer quotes is fine because the format-spec is in a nested expression.
t'{t'{z=:hy "user"}'} \'\'\''
# We have to be careful about changing the quotes if the t-string has a debug expression because it is inserted verbatim.
t'{1=: "abcd \'\'}' # Don't change the outer quotes, or it results in a syntax error
t'{1=: abcd \'\'}' # Changing the quotes here is fine because the inner quotes aren't the opposite quotes
t'{1=: abcd \"\"}' # Changing the quotes here is fine because the inner quotes are escaped
# Don't change the quotes in the following cases:
t'{x=:hy "user"} \'\'\''
t'{x=:a{y:hy "user"}} \'\'\''
t'{x=:a{y:{z:hy "user"}}} \'\'\''
t'{x:a{y=:{z:hy "user"}}} \'\'\''
# This is fine because the debug expression and format spec are in a nested expression
t"""{1=: "this" is fine}"""
t'''{1=: "this" is fine}''' # Change quotes to double quotes because they're preferred
t'{1=: {'ab"cd"'}}' # It's okay if the quotes are in an expression part.
# Regression tests for https://github.com/astral-sh/ruff/issues/15459
print(t"{ {1, 2, 3} - {2} }")
print(t"{ {1: 2}.keys() }")
print(t"{({1, 2, 3}) - ({2})}")
print(t"{1, 2, {3} }")
print(t"{(1, 2, {3})}")
# Regression tests for https://github.com/astral-sh/ruff/issues/15535
print(t"{ {}, }") # A single item tuple gets parenthesized
print(t"{ {}.values(), }")
print(t"{ {}, 1 }") # A tuple with multiple elements doesn't get parenthesized
print(t"{ # Tuple with multiple elements that doesn't fit on a single line gets parenthesized
{}, 1,
}")
# Regression tests for https://github.com/astral-sh/ruff/issues/15536
print(t"{ {}, 1, }")

View file

@ -205,14 +205,14 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> {
pub(crate) fn finish(&mut self) -> FormatResult<()> {
self.result.and_then(|()| {
// Don't add a magic trailing comma when formatting an f-string expression
// Don't add a magic trailing comma when formatting an f-string or t-string expression
// that always must be flat because the `expand_parent` forces enclosing
// groups to expand, e.g. `print(f"{(a,)} ")` would format the f-string in
// flat mode but the `print` call gets expanded because of the `expand_parent`.
if self
.fmt
.context()
.f_string_state()
.interpolated_string_state()
.can_contain_line_breaks()
== Some(false)
{

View file

@ -314,15 +314,14 @@ fn handle_enclosed_comment<'a>(
AnyNodeRef::StmtImportFrom(import_from) => handle_import_from_comment(comment, import_from),
AnyNodeRef::StmtWith(with_) => handle_with_comment(comment, with_),
AnyNodeRef::ExprCall(_) => handle_call_comment(comment),
AnyNodeRef::ExprStringLiteral(_) => {
if let Some(AnyNodeRef::FString(fstring)) = comment.enclosing_parent() {
CommentPlacement::dangling(fstring, comment)
} else {
CommentPlacement::Default(comment)
}
}
AnyNodeRef::ExprStringLiteral(_) => match comment.enclosing_parent() {
Some(AnyNodeRef::FString(fstring)) => CommentPlacement::dangling(fstring, comment),
Some(AnyNodeRef::TString(tstring)) => CommentPlacement::dangling(tstring, comment),
_ => CommentPlacement::Default(comment),
},
AnyNodeRef::FString(fstring) => CommentPlacement::dangling(fstring, comment),
AnyNodeRef::FStringExpressionElement(_) => {
AnyNodeRef::TString(tstring) => CommentPlacement::dangling(tstring, comment),
AnyNodeRef::InterpolatedElement(_) => {
// Handle comments after the format specifier (should be rare):
//
// ```python
@ -336,7 +335,8 @@ fn handle_enclosed_comment<'a>(
if matches!(
comment.preceding_node(),
Some(
AnyNodeRef::FStringExpressionElement(_) | AnyNodeRef::FStringLiteralElement(_)
AnyNodeRef::InterpolatedElement(_)
| AnyNodeRef::InterpolatedStringLiteralElement(_)
)
) {
CommentPlacement::trailing(comment.enclosing_node(), comment)
@ -344,6 +344,7 @@ fn handle_enclosed_comment<'a>(
handle_bracketed_end_of_line_comment(comment, source)
}
}
AnyNodeRef::ExprList(_)
| AnyNodeRef::ExprSet(_)
| AnyNodeRef::ExprListComp(_)

View file

@ -7,7 +7,7 @@ use ruff_python_parser::Tokens;
use crate::PyFormatOptions;
use crate::comments::Comments;
use crate::other::f_string_element::FStringExpressionElementContext;
use crate::other::interpolated_string_element::InterpolatedElementContext;
pub struct PyFormatContext<'a> {
options: PyFormatOptions,
@ -25,8 +25,8 @@ pub struct PyFormatContext<'a> {
/// quote style that is inverted from the one here in order to ensure that
/// the formatted Python code will be valid.
docstring: Option<Quote>,
/// The state of the formatter with respect to f-strings.
f_string_state: FStringState,
/// The state of the formatter with respect to f-strings and t-strings.
interpolated_string_state: InterpolatedStringState,
}
impl<'a> PyFormatContext<'a> {
@ -44,7 +44,7 @@ impl<'a> PyFormatContext<'a> {
node_level: NodeLevel::TopLevel(TopLevelStatementPosition::Other),
indent_level: IndentLevel::new(0),
docstring: None,
f_string_state: FStringState::Outside,
interpolated_string_state: InterpolatedStringState::Outside,
}
}
@ -97,12 +97,15 @@ impl<'a> PyFormatContext<'a> {
}
}
pub(crate) fn f_string_state(&self) -> FStringState {
self.f_string_state
pub(crate) fn interpolated_string_state(&self) -> InterpolatedStringState {
self.interpolated_string_state
}
pub(crate) fn set_f_string_state(&mut self, f_string_state: FStringState) {
self.f_string_state = f_string_state;
pub(crate) fn set_interpolated_string_state(
&mut self,
interpolated_string_state: InterpolatedStringState,
) {
self.interpolated_string_state = interpolated_string_state;
}
/// Returns `true` if preview mode is enabled.
@ -135,24 +138,24 @@ impl Debug for PyFormatContext<'_> {
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) enum FStringState {
pub(crate) enum InterpolatedStringState {
/// The formatter is inside an f-string expression element i.e., between the
/// curly brace in `f"foo {x}"`.
///
/// The containing `FStringContext` is the surrounding f-string context.
InsideExpressionElement(FStringExpressionElementContext),
InsideInterpolatedElement(InterpolatedElementContext),
/// The formatter is outside an f-string.
#[default]
Outside,
}
impl FStringState {
impl InterpolatedStringState {
pub(crate) fn can_contain_line_breaks(self) -> Option<bool> {
match self {
FStringState::InsideExpressionElement(context) => {
InterpolatedStringState::InsideInterpolatedElement(context) => {
Some(context.can_contain_line_breaks())
}
FStringState::Outside => None,
InterpolatedStringState::Outside => None,
}
}
}
@ -375,25 +378,25 @@ where
}
}
pub(crate) struct WithFStringState<'a, B, D>
pub(crate) struct WithInterpolatedStringState<'a, B, D>
where
D: DerefMut<Target = B>,
B: Buffer<Context = PyFormatContext<'a>>,
{
buffer: D,
saved_location: FStringState,
saved_location: InterpolatedStringState,
}
impl<'a, B, D> WithFStringState<'a, B, D>
impl<'a, B, D> WithInterpolatedStringState<'a, B, D>
where
D: DerefMut<Target = B>,
B: Buffer<Context = PyFormatContext<'a>>,
{
pub(crate) fn new(expr_location: FStringState, mut buffer: D) -> Self {
pub(crate) fn new(expr_location: InterpolatedStringState, mut buffer: D) -> Self {
let context = buffer.state_mut().context_mut();
let saved_location = context.f_string_state();
let saved_location = context.interpolated_string_state();
context.set_f_string_state(expr_location);
context.set_interpolated_string_state(expr_location);
Self {
buffer,
@ -402,7 +405,7 @@ where
}
}
impl<'a, B, D> Deref for WithFStringState<'a, B, D>
impl<'a, B, D> Deref for WithInterpolatedStringState<'a, B, D>
where
D: DerefMut<Target = B>,
B: Buffer<Context = PyFormatContext<'a>>,
@ -414,7 +417,7 @@ where
}
}
impl<'a, B, D> DerefMut for WithFStringState<'a, B, D>
impl<'a, B, D> DerefMut for WithInterpolatedStringState<'a, B, D>
where
D: DerefMut<Target = B>,
B: Buffer<Context = PyFormatContext<'a>>,
@ -424,7 +427,7 @@ where
}
}
impl<'a, B, D> Drop for WithFStringState<'a, B, D>
impl<'a, B, D> Drop for WithInterpolatedStringState<'a, B, D>
where
D: DerefMut<Target = B>,
B: Buffer<Context = PyFormatContext<'a>>,
@ -433,6 +436,6 @@ where
self.buffer
.state_mut()
.context_mut()
.set_f_string_state(self.saved_location);
.set_interpolated_string_state(self.saved_location);
}
}

View file

@ -3,7 +3,7 @@ use ruff_python_ast::{AnyNodeRef, ExprFString, StringLike};
use crate::expression::parentheses::{
NeedsParentheses, OptionalParentheses, in_parentheses_only_group,
};
use crate::other::f_string::FStringLayout;
use crate::other::interpolated_string::InterpolatedStringLayout;
use crate::prelude::*;
use crate::string::StringLikeExtensions;
use crate::string::implicit::{
@ -41,7 +41,11 @@ impl NeedsParentheses for ExprFString {
if let Some(fstring_part) = self.as_single_part_fstring() {
// The f-string is not implicitly concatenated
if StringLike::FString(self).is_multiline(context)
|| FStringLayout::from_f_string(fstring_part, context.source()).is_multiline()
|| InterpolatedStringLayout::from_interpolated_string_elements(
&fstring_part.elements,
context.source(),
)
.is_multiline()
{
OptionalParentheses::Never
} else {

View file

@ -0,0 +1,59 @@
use ruff_python_ast::{AnyNodeRef, ExprTString, StringLike};
use crate::expression::parentheses::{
NeedsParentheses, OptionalParentheses, in_parentheses_only_group,
};
use crate::other::interpolated_string::InterpolatedStringLayout;
use crate::prelude::*;
use crate::string::StringLikeExtensions;
use crate::string::implicit::{
FormatImplicitConcatenatedString, FormatImplicitConcatenatedStringFlat,
};
#[derive(Default)]
pub struct FormatExprTString;
impl FormatNodeRule<ExprTString> for FormatExprTString {
fn fmt_fields(&self, item: &ExprTString, f: &mut PyFormatter) -> FormatResult<()> {
if let Some(t_string) = item.as_single_part_tstring() {
t_string.format().fmt(f)
} else {
// Always join tstrings that aren't parenthesized and thus, are always on a single line.
if !f.context().node_level().is_parenthesized() {
if let Some(format_flat) =
FormatImplicitConcatenatedStringFlat::new(item.into(), f.context())
{
return format_flat.fmt(f);
}
}
in_parentheses_only_group(&FormatImplicitConcatenatedString::new(item)).fmt(f)
}
}
}
impl NeedsParentheses for ExprTString {
fn needs_parentheses(
&self,
_parent: AnyNodeRef,
context: &PyFormatContext,
) -> OptionalParentheses {
if let Some(tstring_part) = self.as_single_part_tstring() {
// The t-string is not implicitly concatenated
if StringLike::TString(self).is_multiline(context)
|| InterpolatedStringLayout::from_interpolated_string_elements(
&tstring_part.elements,
context.source(),
)
.is_multiline()
{
OptionalParentheses::Never
} else {
OptionalParentheses::BestFit
}
} else {
// The t-string is implicitly concatenated
OptionalParentheses::Multiline
}
}
}

View file

@ -50,6 +50,7 @@ pub(crate) mod expr_slice;
pub(crate) mod expr_starred;
pub(crate) mod expr_string_literal;
pub(crate) mod expr_subscript;
pub(crate) mod expr_t_string;
pub(crate) mod expr_tuple;
pub(crate) mod expr_unary_op;
pub(crate) mod expr_yield;
@ -94,6 +95,7 @@ impl FormatRule<Expr, PyFormatContext<'_>> for FormatExpr {
Expr::Compare(expr) => expr.format().fmt(f),
Expr::Call(expr) => expr.format().fmt(f),
Expr::FString(expr) => expr.format().fmt(f),
Expr::TString(expr) => expr.format().fmt(f),
Expr::StringLiteral(expr) => expr.format().fmt(f),
Expr::BytesLiteral(expr) => expr.format().fmt(f),
Expr::NumberLiteral(expr) => expr.format().fmt(f),
@ -282,6 +284,7 @@ fn format_with_parentheses_comments(
Expr::Compare(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f),
Expr::Call(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f),
Expr::FString(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f),
Expr::TString(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f),
Expr::StringLiteral(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f),
Expr::BytesLiteral(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f),
Expr::NumberLiteral(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f),
@ -480,6 +483,7 @@ impl NeedsParentheses for Expr {
Expr::Compare(expr) => expr.needs_parentheses(parent, context),
Expr::Call(expr) => expr.needs_parentheses(parent, context),
Expr::FString(expr) => expr.needs_parentheses(parent, context),
Expr::TString(expr) => expr.needs_parentheses(parent, context),
Expr::StringLiteral(expr) => expr.needs_parentheses(parent, context),
Expr::BytesLiteral(expr) => expr.needs_parentheses(parent, context),
Expr::NumberLiteral(expr) => expr.needs_parentheses(parent, context),
@ -775,6 +779,7 @@ impl<'input> CanOmitOptionalParenthesesVisitor<'input> {
// Terminal nodes or nodes that wrap a sub-expression (where the sub expression can never be at the end).
Expr::FString(_)
| Expr::TString(_)
| Expr::StringLiteral(_)
| Expr::BytesLiteral(_)
| Expr::NumberLiteral(_)
@ -1126,6 +1131,7 @@ pub(crate) fn is_expression_huggable(expr: &Expr, context: &PyFormatContext) ->
| Expr::StringLiteral(_)
| Expr::BytesLiteral(_)
| Expr::FString(_)
| Expr::TString(_)
| Expr::EllipsisLiteral(_) => false,
}
}
@ -1221,6 +1227,7 @@ pub(crate) fn is_splittable_expression(expr: &Expr, context: &PyFormatContext) -
// String like literals can expand if they are implicit concatenated.
Expr::FString(fstring) => fstring.value.is_implicit_concatenated(),
Expr::TString(tstring) => tstring.value.is_implicit_concatenated(),
Expr::StringLiteral(string) => string.value.is_implicit_concatenated(),
Expr::BytesLiteral(bytes) => bytes.value.is_implicit_concatenated(),
@ -1278,6 +1285,7 @@ pub(crate) fn left_most<'expr>(
| Expr::Name(_)
| Expr::Starred(_)
| Expr::FString(_)
| Expr::TString(_)
| Expr::StringLiteral(_)
| Expr::BytesLiteral(_)
| Expr::NumberLiteral(_)

View file

@ -1562,6 +1562,42 @@ impl<'ast> IntoFormat<PyFormatContext<'ast>> for ast::ExprFString {
}
}
impl FormatRule<ast::ExprTString, PyFormatContext<'_>>
for crate::expression::expr_t_string::FormatExprTString
{
#[inline]
fn fmt(&self, node: &ast::ExprTString, f: &mut PyFormatter) -> FormatResult<()> {
FormatNodeRule::<ast::ExprTString>::fmt(self, node, f)
}
}
impl<'ast> AsFormat<PyFormatContext<'ast>> for ast::ExprTString {
type Format<'a> = FormatRefWithRule<
'a,
ast::ExprTString,
crate::expression::expr_t_string::FormatExprTString,
PyFormatContext<'ast>,
>;
fn format(&self) -> Self::Format<'_> {
FormatRefWithRule::new(
self,
crate::expression::expr_t_string::FormatExprTString::default(),
)
}
}
impl<'ast> IntoFormat<PyFormatContext<'ast>> for ast::ExprTString {
type Format = FormatOwnedWithRule<
ast::ExprTString,
crate::expression::expr_t_string::FormatExprTString,
PyFormatContext<'ast>,
>;
fn into_format(self) -> Self::Format {
FormatOwnedWithRule::new(
self,
crate::expression::expr_t_string::FormatExprTString::default(),
)
}
}
impl FormatRule<ast::ExprStringLiteral, PyFormatContext<'_>>
for crate::expression::expr_string_literal::FormatExprStringLiteral
{
@ -2963,6 +2999,34 @@ impl<'ast> IntoFormat<PyFormatContext<'ast>> for ast::FString {
}
}
impl FormatRule<ast::TString, PyFormatContext<'_>> for crate::other::t_string::FormatTString {
#[inline]
fn fmt(&self, node: &ast::TString, f: &mut PyFormatter) -> FormatResult<()> {
FormatNodeRule::<ast::TString>::fmt(self, node, f)
}
}
impl<'ast> AsFormat<PyFormatContext<'ast>> for ast::TString {
type Format<'a> = FormatRefWithRule<
'a,
ast::TString,
crate::other::t_string::FormatTString,
PyFormatContext<'ast>,
>;
fn format(&self) -> Self::Format<'_> {
FormatRefWithRule::new(self, crate::other::t_string::FormatTString::default())
}
}
impl<'ast> IntoFormat<PyFormatContext<'ast>> for ast::TString {
type Format = FormatOwnedWithRule<
ast::TString,
crate::other::t_string::FormatTString,
PyFormatContext<'ast>,
>;
fn into_format(self) -> Self::Format {
FormatOwnedWithRule::new(self, crate::other::t_string::FormatTString::default())
}
}
impl FormatRule<ast::StringLiteral, PyFormatContext<'_>>
for crate::other::string_literal::FormatStringLiteral
{

View file

@ -1,12 +1,9 @@
use ruff_formatter::write;
use ruff_python_ast::{AnyStringFlags, FString, StringFlags};
use ruff_source_file::LineRanges;
use ruff_text_size::Ranged;
use super::interpolated_string_element::FormatInterpolatedStringElement;
use crate::other::interpolated_string::{InterpolatedStringContext, InterpolatedStringLayout};
use crate::prelude::*;
use crate::string::{StringNormalizer, StringQuotes};
use super::f_string_element::FormatFStringElement;
use ruff_formatter::write;
use ruff_python_ast::{FString, StringFlags};
/// Formats an f-string which is part of a larger f-string expression.
///
@ -21,9 +18,12 @@ impl FormatNodeRule<FString> for FormatFString {
let string_kind = normalizer.choose_quotes(item.into()).flags();
let context = FStringContext::new(
let context = InterpolatedStringContext::new(
string_kind,
FStringLayout::from_f_string(item, f.context().source()),
InterpolatedStringLayout::from_interpolated_string_elements(
&item.elements,
f.context().source(),
),
);
// Starting prefix and quote
@ -31,78 +31,10 @@ impl FormatNodeRule<FString> for FormatFString {
write!(f, [string_kind.prefix(), quotes])?;
for element in &item.elements {
FormatFStringElement::new(element, context).fmt(f)?;
FormatInterpolatedStringElement::new(element, context).fmt(f)?;
}
// Ending quote
quotes.fmt(f)
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct FStringContext {
/// The string flags of the enclosing f-string part.
enclosing_flags: AnyStringFlags,
layout: FStringLayout,
}
impl FStringContext {
pub(crate) const fn new(flags: AnyStringFlags, layout: FStringLayout) -> Self {
Self {
enclosing_flags: flags,
layout,
}
}
pub(crate) fn flags(self) -> AnyStringFlags {
self.enclosing_flags
}
pub(crate) const fn layout(self) -> FStringLayout {
self.layout
}
}
#[derive(Copy, Clone, Debug)]
pub(crate) enum FStringLayout {
/// Original f-string is flat.
/// Don't break expressions to keep the string flat.
Flat,
/// Original f-string has multiline expressions in the replacement fields.
/// Allow breaking expressions across multiple lines.
Multiline,
}
impl FStringLayout {
pub(crate) fn from_f_string(f_string: &FString, source: &str) -> Self {
// Heuristic: Allow breaking the f-string expressions across multiple lines
// only if there already is at least one multiline expression. This puts the
// control in the hands of the user to decide if they want to break the
// f-string expressions across multiple lines or not. This is similar to
// how Prettier does it for template literals in JavaScript.
//
// If it's single quoted f-string and it contains a multiline expression, then we
// assume that the target version of Python supports it (3.12+). If there are comments
// used in any of the expression of the f-string, then it's always going to be multiline
// and we assume that the target version of Python supports it (3.12+).
//
// Reference: https://prettier.io/docs/en/next/rationale.html#template-literals
if f_string
.elements
.expressions()
.any(|expr| source.contains_line_break(expr.range()))
{
Self::Multiline
} else {
Self::Flat
}
}
pub(crate) const fn is_flat(self) -> bool {
matches!(self, FStringLayout::Flat)
}
pub(crate) const fn is_multiline(self) -> bool {
matches!(self, FStringLayout::Multiline)
}
}

View file

@ -0,0 +1,73 @@
use ruff_python_ast::{AnyStringFlags, InterpolatedStringElements};
use ruff_source_file::LineRanges;
use ruff_text_size::Ranged;
#[derive(Clone, Copy, Debug)]
pub(crate) struct InterpolatedStringContext {
/// The string flags of the enclosing f/t-string part.
enclosing_flags: AnyStringFlags,
layout: InterpolatedStringLayout,
}
impl InterpolatedStringContext {
pub(crate) const fn new(flags: AnyStringFlags, layout: InterpolatedStringLayout) -> Self {
Self {
enclosing_flags: flags,
layout,
}
}
pub(crate) fn flags(self) -> AnyStringFlags {
self.enclosing_flags
}
pub(crate) const fn layout(self) -> InterpolatedStringLayout {
self.layout
}
}
#[derive(Copy, Clone, Debug)]
pub(crate) enum InterpolatedStringLayout {
/// Original f/t-string is flat.
/// Don't break expressions to keep the string flat.
Flat,
/// Original f/t-string has multiline expressions in the replacement fields.
/// Allow breaking expressions across multiple lines.
Multiline,
}
impl InterpolatedStringLayout {
// Heuristic: Allow breaking the f/t-string expressions across multiple lines
// only if there already is at least one multiline expression. This puts the
// control in the hands of the user to decide if they want to break the
// f/t-string expressions across multiple lines or not. This is similar to
// how Prettier does it for template literals in JavaScript.
//
// If it's single quoted f-string and it contains a multiline expression, then we
// assume that the target version of Python supports it (3.12+). If there are comments
// used in any of the expression of the f-string, then it's always going to be multiline
// and we assume that the target version of Python supports it (3.12+).
//
// Reference: https://prettier.io/docs/en/next/rationale.html#template-literals
pub(crate) fn from_interpolated_string_elements(
elements: &InterpolatedStringElements,
source: &str,
) -> Self {
if elements
.interpolations()
.any(|expr| source.contains_line_break(expr.range()))
{
Self::Multiline
} else {
Self::Flat
}
}
pub(crate) const fn is_flat(self) -> bool {
matches!(self, InterpolatedStringLayout::Flat)
}
pub(crate) const fn is_multiline(self) -> bool {
matches!(self, InterpolatedStringLayout::Multiline)
}
}

View file

@ -2,42 +2,47 @@ use std::borrow::Cow;
use ruff_formatter::{Buffer, RemoveSoftLinesBuffer, format_args, write};
use ruff_python_ast::{
AnyStringFlags, ConversionFlag, Expr, FStringElement, FStringExpressionElement,
FStringLiteralElement, StringFlags,
AnyStringFlags, ConversionFlag, Expr, InterpolatedElement, InterpolatedStringElement,
InterpolatedStringLiteralElement, StringFlags,
};
use ruff_text_size::{Ranged, TextSlice};
use crate::comments::{dangling_open_parenthesis_comments, trailing_comments};
use crate::context::{FStringState, NodeLevel, WithFStringState, WithNodeLevel};
use crate::context::{
InterpolatedStringState, NodeLevel, WithInterpolatedStringState, WithNodeLevel,
};
use crate::expression::left_most;
use crate::prelude::*;
use crate::string::normalize_string;
use crate::verbatim::verbatim_text;
use super::f_string::FStringContext;
use super::interpolated_string::InterpolatedStringContext;
/// Formats an f-string element which is either a literal or a formatted expression.
///
/// This delegates the actual formatting to the appropriate formatter.
pub(crate) struct FormatFStringElement<'a> {
element: &'a FStringElement,
context: FStringContext,
pub(crate) struct FormatInterpolatedStringElement<'a> {
element: &'a InterpolatedStringElement,
context: InterpolatedStringContext,
}
impl<'a> FormatFStringElement<'a> {
pub(crate) fn new(element: &'a FStringElement, context: FStringContext) -> Self {
impl<'a> FormatInterpolatedStringElement<'a> {
pub(crate) fn new(
element: &'a InterpolatedStringElement,
context: InterpolatedStringContext,
) -> Self {
Self { element, context }
}
}
impl Format<PyFormatContext<'_>> for FormatFStringElement<'_> {
impl Format<PyFormatContext<'_>> for FormatInterpolatedStringElement<'_> {
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
match self.element {
FStringElement::Literal(string_literal) => {
InterpolatedStringElement::Literal(string_literal) => {
FormatFStringLiteralElement::new(string_literal, self.context.flags()).fmt(f)
}
FStringElement::Expression(expression) => {
FormatFStringExpressionElement::new(expression, self.context).fmt(f)
InterpolatedStringElement::Interpolation(expression) => {
FormatInterpolatedElement::new(expression, self.context).fmt(f)
}
}
}
@ -45,13 +50,16 @@ impl Format<PyFormatContext<'_>> for FormatFStringElement<'_> {
/// Formats an f-string literal element.
pub(crate) struct FormatFStringLiteralElement<'a> {
element: &'a FStringLiteralElement,
element: &'a InterpolatedStringLiteralElement,
/// Flags of the enclosing F-string part
fstring_flags: AnyStringFlags,
}
impl<'a> FormatFStringLiteralElement<'a> {
pub(crate) fn new(element: &'a FStringLiteralElement, fstring_flags: AnyStringFlags) -> Self {
pub(crate) fn new(
element: &'a InterpolatedStringLiteralElement,
fstring_flags: AnyStringFlags,
) -> Self {
Self {
element,
fstring_flags,
@ -72,16 +80,16 @@ impl Format<PyFormatContext<'_>> for FormatFStringLiteralElement<'_> {
/// Context representing an f-string expression element.
#[derive(Clone, Copy, Debug)]
pub(crate) struct FStringExpressionElementContext {
pub(crate) struct InterpolatedElementContext {
/// The context of the parent f-string containing this expression element.
parent_context: FStringContext,
parent_context: InterpolatedStringContext,
/// Indicates whether this expression element has format specifier or not.
has_format_spec: bool,
}
impl FStringExpressionElementContext {
/// Returns the [`FStringContext`] containing this expression element.
pub(crate) fn f_string(self) -> FStringContext {
impl InterpolatedElementContext {
/// Returns the [`InterpolatedStringContext`] containing this expression element.
pub(crate) fn interpolated_string(self) -> InterpolatedStringContext {
self.parent_context
}
@ -113,16 +121,19 @@ impl FStringExpressionElementContext {
}
/// Formats an f-string expression element.
pub(crate) struct FormatFStringExpressionElement<'a> {
element: &'a FStringExpressionElement,
context: FStringExpressionElementContext,
pub(crate) struct FormatInterpolatedElement<'a> {
element: &'a InterpolatedElement,
context: InterpolatedElementContext,
}
impl<'a> FormatFStringExpressionElement<'a> {
pub(crate) fn new(element: &'a FStringExpressionElement, context: FStringContext) -> Self {
impl<'a> FormatInterpolatedElement<'a> {
pub(crate) fn new(
element: &'a InterpolatedElement,
context: InterpolatedStringContext,
) -> Self {
Self {
element,
context: FStringExpressionElementContext {
context: InterpolatedElementContext {
parent_context: context,
has_format_spec: element.format_spec.is_some(),
},
@ -130,9 +141,9 @@ impl<'a> FormatFStringExpressionElement<'a> {
}
}
impl Format<PyFormatContext<'_>> for FormatFStringExpressionElement<'_> {
impl Format<PyFormatContext<'_>> for FormatInterpolatedElement<'_> {
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
let FStringExpressionElement {
let InterpolatedElement {
expression,
debug_text,
conversion,
@ -214,8 +225,8 @@ impl Format<PyFormatContext<'_>> for FormatFStringExpressionElement<'_> {
let item = format_with(|f: &mut PyFormatter| {
// Update the context to be inside the f-string expression element.
let f = &mut WithFStringState::new(
FStringState::InsideExpressionElement(self.context),
let f = &mut WithInterpolatedStringState::new(
InterpolatedStringState::InsideInterpolatedElement(self.context),
f,
);
@ -233,7 +244,11 @@ impl Format<PyFormatContext<'_>> for FormatFStringExpressionElement<'_> {
token(":").fmt(f)?;
for element in &format_spec.elements {
FormatFStringElement::new(element, self.context.f_string()).fmt(f)?;
FormatInterpolatedStringElement::new(
element,
self.context.interpolated_string(),
)
.fmt(f)?;
}
// These trailing comments can only occur if the format specifier is

View file

@ -7,12 +7,14 @@ pub(crate) mod decorator;
pub(crate) mod elif_else_clause;
pub(crate) mod except_handler_except_handler;
pub(crate) mod f_string;
pub(crate) mod f_string_element;
pub(crate) mod identifier;
pub(crate) mod interpolated_string;
pub(crate) mod interpolated_string_element;
pub(crate) mod keyword;
pub(crate) mod match_case;
pub(crate) mod parameter;
pub(crate) mod parameter_with_default;
pub(crate) mod parameters;
pub(crate) mod string_literal;
pub(crate) mod t_string;
pub(crate) mod with_item;

View file

@ -0,0 +1,40 @@
use super::interpolated_string_element::FormatInterpolatedStringElement;
use crate::other::interpolated_string::{InterpolatedStringContext, InterpolatedStringLayout};
use crate::prelude::*;
use crate::string::{StringNormalizer, StringQuotes};
use ruff_formatter::write;
use ruff_python_ast::{StringFlags, TString};
/// Formats a t-string which is part of a larger t-string expression.
///
/// For example, this would be used to format the t-string part in `"foo" t"bar {x}"`
/// or the standalone t-string in `t"foo {x} bar"`.
#[derive(Default)]
pub struct FormatTString;
impl FormatNodeRule<TString> for FormatTString {
fn fmt_fields(&self, item: &TString, f: &mut PyFormatter) -> FormatResult<()> {
let normalizer = StringNormalizer::from_context(f.context());
let string_kind = normalizer.choose_quotes(item.into()).flags();
let context = InterpolatedStringContext::new(
string_kind,
InterpolatedStringLayout::from_interpolated_string_elements(
&item.elements,
f.context().source(),
),
);
// Starting prefix and quote
let quotes = StringQuotes::from(string_kind);
write!(f, [string_kind.prefix(), quotes])?;
for element in &item.elements {
FormatInterpolatedStringElement::new(element, context).fmt(f)?;
}
// Ending quote
quotes.fmt(f)
}
}

View file

@ -293,6 +293,7 @@ impl<'a> CanOmitOptionalParenthesesVisitor<'a> {
// F-strings are allowed according to python's grammar but fail with a syntax error at runtime.
// That's why we need to support them for formatting.
Expr::FString(_) |
Expr::TString(_)|
Expr::NumberLiteral(_) | Expr::Attribute(_) | Expr::UnaryOp(_) => {
// require no state update other than visit_pattern does.
}
@ -306,7 +307,7 @@ impl<'a> CanOmitOptionalParenthesesVisitor<'a> {
_ => {
debug_assert!(
false,
"Unsupported expression in pattern mach value: {:?}",
"Unsupported expression in pattern match value: {:?}",
value.value
);
}

View file

@ -659,10 +659,11 @@ impl Format<PyFormatContext<'_>> for FormatEnclosingNode<'_> {
| AnyNodeRef::ExprYieldFrom(_)
| AnyNodeRef::ExprCompare(_)
| AnyNodeRef::ExprCall(_)
| AnyNodeRef::FStringExpressionElement(_)
| AnyNodeRef::FStringLiteralElement(_)
| AnyNodeRef::FStringFormatSpec(_)
| AnyNodeRef::InterpolatedElement(_)
| AnyNodeRef::InterpolatedStringLiteralElement(_)
| AnyNodeRef::InterpolatedStringFormatSpec(_)
| AnyNodeRef::ExprFString(_)
| AnyNodeRef::ExprTString(_)
| AnyNodeRef::ExprStringLiteral(_)
| AnyNodeRef::ExprBytesLiteral(_)
| AnyNodeRef::ExprNumberLiteral(_)
@ -679,6 +680,7 @@ impl Format<PyFormatContext<'_>> for FormatEnclosingNode<'_> {
| AnyNodeRef::ExprIpyEscapeCommand(_)
| AnyNodeRef::FString(_)
| AnyNodeRef::StringLiteral(_)
| AnyNodeRef::TString(_)
| AnyNodeRef::PatternMatchValue(_)
| AnyNodeRef::PatternMatchSingleton(_)
| AnyNodeRef::PatternMatchSequence(_)

View file

@ -1,6 +1,6 @@
use ruff_formatter::{FormatError, RemoveSoftLinesBuffer, format_args, write};
use ruff_python_ast::{
AnyNodeRef, Expr, ExprAttribute, ExprCall, FString, Operator, StmtAssign, StringLike,
AnyNodeRef, Expr, ExprAttribute, ExprCall, FString, Operator, StmtAssign, StringLike, TString,
TypeParams,
};
@ -17,7 +17,7 @@ use crate::expression::{
can_omit_optional_parentheses, has_own_parentheses, has_parentheses,
maybe_parenthesize_expression,
};
use crate::other::f_string::FStringLayout;
use crate::other::interpolated_string::InterpolatedStringLayout;
use crate::statement::trailing_semicolon;
use crate::string::StringLikeExtensions;
use crate::string::implicit::{
@ -291,15 +291,16 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
let can_inline_comment = should_inline_comments(value, *statement, f.context());
let string_like = StringLike::try_from(*value).ok();
let format_f_string =
string_like.and_then(|string| format_f_string_assignment(string, f.context()));
let format_interpolated_string = string_like
.and_then(|string| format_interpolated_string_assignment(string, f.context()));
let format_implicit_flat = string_like.and_then(|string| {
FormatImplicitConcatenatedStringFlat::new(string, f.context())
});
if !can_inline_comment
&& format_implicit_flat.is_none()
&& format_f_string.is_none()
&& format_interpolated_string.is_none()
{
return maybe_parenthesize_expression(
value,
@ -351,7 +352,7 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
let string = flat.string();
let flat = format_with(|f| {
if string.is_fstring() {
if string.is_interpolated_string() {
let mut buffer = RemoveSoftLinesBuffer::new(&mut *f);
write!(buffer, [flat])
@ -361,7 +362,7 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
})
.memoized();
// F-String containing an expression with a magic trailing comma, a comment, or a
// F-string or T-string containing an expression with a magic trailing comma, a comment, or a
// multiline debug expression should never be joined. Use the default layout.
// ```python
// aaaa = f"abcd{[
@ -369,7 +370,7 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
// 2,
// ]}" "more"
// ```
if string.is_fstring() && flat.inspect(f)?.will_break() {
if string.is_interpolated_string() && flat.inspect(f)?.will_break() {
inline_comments.mark_unformatted();
return write!(
@ -446,24 +447,23 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
best_fitting![single_line, joined_parenthesized, implicit_expanded]
.with_mode(BestFittingMode::AllLines)
.fmt(f)?;
} else if let Some(format_f_string) = format_f_string {
} else if let Some(format_interpolated_string) = format_interpolated_string {
inline_comments.mark_formatted();
let f_string_flat = format_with(|f| {
let interpolated_string_flat = format_with(|f| {
let mut buffer = RemoveSoftLinesBuffer::new(&mut *f);
write!(buffer, [format_f_string.format()])
write!(buffer, [format_interpolated_string])
})
.memoized();
// F-String containing an expression with a magic trailing comma, a comment, or a
// multiline debug expression should never be joined. Use the default layout.
// F/T-String containing an interpolation with a magic trailing comma, a comment, or a
// multiline debug interpolation should never be joined. Use the default layout.
// ```python
// aaaa = f"aaaa {[
// 1, 2,
// ]} bbbb"
// ```
if f_string_flat.inspect(f)?.will_break() {
if interpolated_string_flat.inspect(f)?.will_break() {
inline_comments.mark_unformatted();
return write!(
@ -482,23 +482,26 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
// expression}moreeeeeeeeeeeeeeeee"
// ```
// Flatten the f-string.
// Flatten the f/t-string.
// ```python
// aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee"
// ```
let single_line =
format_with(|f| write!(f, [f_string_flat, inline_comments]));
format_with(|f| write!(f, [interpolated_string_flat, inline_comments]));
// Parenthesize the f-string and flatten the f-string.
// Parenthesize the t-string and flatten the t-string.
// ```python
// aaaaaaaaaaaaaaaaaa = (
// f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee"
// t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee"
// )
// ```
let joined_parenthesized = format_with(|f| {
group(&format_args![
token("("),
soft_block_indent(&format_args![f_string_flat, inline_comments]),
soft_block_indent(&format_args![
interpolated_string_flat,
inline_comments
]),
token(")"),
])
.with_id(Some(group_id))
@ -506,19 +509,24 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
.fmt(f)
});
// Avoid flattening or parenthesizing the f-string, keep the original
// f-string formatting.
// Avoid flattening or parenthesizing the f/t-string, keep the original
// f/t-string formatting.
// ```python
// aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
// aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
// expression
// }moreeeeeeeeeeeeeeeee"
// ```
let format_f_string =
format_with(|f| write!(f, [format_f_string.format(), inline_comments]));
let format_interpolated_string = format_with(|f| {
write!(f, [format_interpolated_string, inline_comments])
});
best_fitting![single_line, joined_parenthesized, format_f_string]
.with_mode(BestFittingMode::AllLines)
.fmt(f)?;
best_fitting![
single_line,
joined_parenthesized,
format_interpolated_string
]
.with_mode(BestFittingMode::AllLines)
.fmt(f)?;
} else {
best_fit_parenthesize(&format_once(|f| {
inline_comments.mark_formatted();
@ -559,17 +567,16 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
let should_inline_comments = should_inline_comments(value, *statement, f.context());
let string_like = StringLike::try_from(*value).ok();
let format_f_string =
string_like.and_then(|string| format_f_string_assignment(string, f.context()));
let format_interpolated_string = string_like
.and_then(|string| format_interpolated_string_assignment(string, f.context()));
let format_implicit_flat = string_like.and_then(|string| {
FormatImplicitConcatenatedStringFlat::new(string, f.context())
});
// Use the normal `maybe_parenthesize_layout` for splittable `value`s.
if !should_inline_comments
&& !should_non_inlineable_use_best_fit(value, *statement, f.context())
&& format_implicit_flat.is_none()
&& format_f_string.is_none()
&& format_interpolated_string.is_none()
{
return write!(
f,
@ -593,7 +600,7 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
// Don't inline comments for attribute and call expressions for black compatibility
let inline_comments = if should_inline_comments
|| format_implicit_flat.is_some()
|| format_f_string.is_some()
|| format_interpolated_string.is_some()
{
OptionalParenthesesInlinedComments::new(
&expression_comments,
@ -633,7 +640,9 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
// This is mainly a performance optimisation that avoids unnecessary memoization
// and using the costly `BestFitting` layout if it is already known that only the last variant
// can ever fit because the left breaks.
if format_implicit_flat.is_none() && format_f_string.is_none() && last_target_breaks
if format_implicit_flat.is_none()
&& format_interpolated_string.is_none()
&& last_target_breaks
{
return write!(
f,
@ -650,7 +659,7 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
let format_value = format_with(|f| {
if let Some(format_implicit_flat) = format_implicit_flat.as_ref() {
if format_implicit_flat.string().is_fstring() {
if format_implicit_flat.string().is_interpolated_string() {
// Remove any soft line breaks emitted by the f-string formatting.
// This is important when formatting f-strings as part of an assignment right side
// because `best_fit_parenthesize` will otherwise still try to break inner
@ -660,11 +669,13 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
} else {
format_implicit_flat.fmt(f)
}
} else if let Some(format_f_string) = format_f_string.as_ref() {
} else if let Some(format_interpolated_string) =
format_interpolated_string.as_ref()
{
// Similar to above, remove any soft line breaks emitted by the f-string
// formatting.
let mut buffer = RemoveSoftLinesBuffer::new(&mut *f);
write!(buffer, [format_f_string.format()])
write!(buffer, [format_interpolated_string])
} else {
value.format().with_options(Parentheses::Never).fmt(f)
}
@ -766,7 +777,7 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
// 2,
// ]}" "more"
// ```
if format_implicit_flat.string().is_fstring()
if format_implicit_flat.string().is_interpolated_string()
&& format_value.inspect(f)?.will_break()
{
inline_comments.mark_unformatted();
@ -905,12 +916,12 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
.with_mode(BestFittingMode::AllLines)
.fmt(f)
}
} else if let Some(format_f_string) = &format_f_string {
// F-String containing an expression with a magic trailing comma, a comment, or a
} else if let Some(format_interpolated_string) = &format_interpolated_string {
// F/T-String containing an interpolation with a magic trailing comma, a comment, or a
// multiline debug expression should never be joined. Use the default layout.
//
// ```python
// aaaa, bbbb = f"aaaa {[
// aaaa, bbbb = t"aaaa {[
// 1, 2,
// ]} bbbb"
// ```
@ -933,40 +944,46 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
);
}
let format_f_string =
format_with(|f| write!(f, [format_f_string.format(), inline_comments]))
let format_interpolated_string =
format_with(|f| write!(f, [format_interpolated_string, inline_comments]))
.memoized();
// Considering the following initial source:
//
// ```python
// aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = (
// f"aaaaaaaaaaaaaaaaaaa {
// t"aaaaaaaaaaaaaaaaaaa {
// aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd"
// )
// ```
//
// Keep the target flat, and use the regular f-string formatting.
// Keep the target flat, and use the regular f/t-string formatting.
//
// ```python
// aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = f"aaaaaaaaaaaaaaaaaaa {
// aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = t"aaaaaaaaaaaaaaaaaaa {
// aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc
// } ddddddddddddddddddd"
// ```
let flat_target_regular_f_string = format_with(|f| {
let flat_target_regular_interpolated_string = format_with(|f| {
write!(
f,
[last_target, space(), operator, space(), format_f_string]
[
last_target,
space(),
operator,
space(),
format_interpolated_string
]
)
});
// Expand the parent and parenthesize the flattened f-string.
// Expand the parent and parenthesize the flattened f/t-string.
//
// ```python
// aaaaaaaaaaaa[
// "bbbbbbbbbbbbbbbb"
// ] = (
// f"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd"
// t"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd"
// )
// ```
let split_target_value_parenthesized_flat = format_with(|f| {
@ -988,16 +1005,16 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
)
});
// Expand the parent, and use the regular f-string formatting.
// Expand the parent, and use the regular f/t-string formatting.
//
// ```python
// aaaaaaaaaaaa[
// "bbbbbbbbbbbbbbbb"
// ] = f"aaaaaaaaaaaaaaaaaaa {
// ] = t"aaaaaaaaaaaaaaaaaaa {
// aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc
// } ddddddddddddddddddd"
// ```
let split_target_regular_f_string = format_with(|f| {
let split_target_regular_interpolated_string = format_with(|f| {
write!(
f,
[
@ -1005,7 +1022,7 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
space(),
operator,
space(),
format_f_string,
format_interpolated_string,
]
)
});
@ -1016,7 +1033,7 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
best_fitting![
split_target_flat_value,
split_target_value_parenthesized_flat,
split_target_regular_f_string,
split_target_regular_interpolated_string,
]
.with_mode(BestFittingMode::AllLines)
.fmt(f)
@ -1024,10 +1041,10 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
best_fitting![
single_line,
flat_target_parenthesize_value,
flat_target_regular_f_string,
flat_target_regular_interpolated_string,
split_target_flat_value,
split_target_value_parenthesized_flat,
split_target_regular_f_string,
split_target_regular_interpolated_string,
]
.with_mode(BestFittingMode::AllLines)
.fmt(f)
@ -1045,13 +1062,31 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
}
}
/// Formats an f-string that is at the value position of an assignment statement.
#[derive(Debug, Copy, Clone)]
enum InterpolatedString<'a> {
FString(&'a FString),
TString(&'a TString),
}
impl Format<PyFormatContext<'_>> for InterpolatedString<'_> {
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
match self {
InterpolatedString::FString(string) => string.format().fmt(f),
InterpolatedString::TString(string) => string.format().fmt(f),
}
}
}
/// Formats an f/t-string that is at the value position of an assignment statement.
///
/// This is just a wrapper around [`FormatFString`] while considering a special case when the
/// f-string is at an assignment statement's value position.
/// For legibility, we discuss only the case of f-strings below, but the
/// same comments apply to t-strings.
///
/// This is necessary to prevent an instability where an f-string contains a multiline expression
/// and the f-string fits on the line, but only when it's surrounded by parentheses.
/// This is just a wrapper around [`FormatFString`] while considering a special
/// case when the f-string is at an assignment statement's value position.
/// This is necessary to prevent an instability where an f-string contains a
/// multiline expression and the f-string fits on the line, but only when it's
/// surrounded by parentheses.
///
/// ```python
/// aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
@ -1099,30 +1134,40 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
/// The reason for this is because (a) f-string already has a multiline expression thus it tries to
/// break the expression and (b) the `BestFit` layout doesn't considers the layout where the
/// multiline f-string isn't surrounded by parentheses.
fn format_f_string_assignment<'a>(
fn format_interpolated_string_assignment<'a>(
string: StringLike<'a>,
context: &PyFormatContext,
) -> Option<&'a FString> {
let StringLike::FString(expr) = string else {
return None;
) -> Option<InterpolatedString<'a>> {
let (interpolated_string, elements) = match string {
StringLike::TString(expr) => {
let t_string = expr.as_single_part_tstring()?;
(InterpolatedString::TString(t_string), &t_string.elements)
}
StringLike::FString(expr) => {
let f_string = expr.as_single_part_fstring()?;
(InterpolatedString::FString(f_string), &f_string.elements)
}
_ => {
return None;
}
};
let f_string = expr.as_single_part_fstring()?;
// If the f-string is flat, there are no breakpoints from which it can be made multiline.
// This is the case when the f-string has no expressions or if it does then the expressions
// If the f/t-string is flat, there are no breakpoints from which it can be made multiline.
// This is the case when the f/t-string has no expressions or if it does then the expressions
// are flat (no newlines).
if FStringLayout::from_f_string(f_string, context.source()).is_flat() {
if InterpolatedStringLayout::from_interpolated_string_elements(elements, context.source())
.is_flat()
{
return None;
}
// This checks whether the f-string is multi-line and it can *never* be flattened. Thus,
// This checks whether the f/t-string is multi-line and it can *never* be flattened. Thus,
// it's useless to try the flattened layout.
if string.is_multiline(context) {
return None;
}
Some(f_string)
Some(interpolated_string)
}
#[derive(Debug, Default)]
@ -1277,6 +1322,9 @@ fn should_inline_comments(
Expr::FString(fstring) => {
fstring.needs_parentheses(parent, context) == OptionalParentheses::BestFit
}
Expr::TString(tstring) => {
tstring.needs_parentheses(parent, context) == OptionalParentheses::BestFit
}
_ => false,
}
}

View file

@ -2,28 +2,31 @@ use itertools::Itertools;
use ruff_formatter::{FormatContext, format_args, write};
use ruff_python_ast::str::{Quote, TripleQuotes};
use ruff_python_ast::str_prefix::{
AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix,
AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix, TStringPrefix,
};
use ruff_python_ast::{
AnyStringFlags, FString, InterpolatedStringElement, StringFlags, StringLike, StringLikePart,
TString,
};
use ruff_python_ast::{AnyStringFlags, FStringElement, StringFlags, StringLike, StringLikePart};
use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextRange};
use std::borrow::Cow;
use crate::comments::{leading_comments, trailing_comments};
use crate::expression::parentheses::in_parentheses_only_soft_line_break_or_space;
use crate::other::f_string::{FStringContext, FStringLayout};
use crate::other::f_string_element::FormatFStringExpressionElement;
use crate::other::interpolated_string::{InterpolatedStringContext, InterpolatedStringLayout};
use crate::other::interpolated_string_element::FormatInterpolatedElement;
use crate::prelude::*;
use crate::string::docstring::needs_chaperone_space;
use crate::string::normalize::{
QuoteMetadata, is_fstring_with_quoted_debug_expression,
is_fstring_with_quoted_format_spec_and_debug,
is_fstring_with_triple_quoted_literal_expression_containing_quotes,
is_interpolated_string_with_quoted_format_spec_and_debug,
};
use crate::string::{StringLikeExtensions, StringNormalizer, StringQuotes, normalize_string};
/// Formats any implicitly concatenated string. This could be any valid combination
/// of string, bytes or f-string literals.
/// of string, bytes, f-string, or t-string literals.
pub(crate) struct FormatImplicitConcatenatedString<'a> {
string: StringLike<'a>,
}
@ -98,6 +101,7 @@ impl Format<PyFormatContext<'_>> for FormatImplicitConcatenatedStringExpanded<'_
StringLikePart::String(part) => part.format().fmt(f),
StringLikePart::Bytes(bytes_literal) => bytes_literal.format().fmt(f),
StringLikePart::FString(part) => part.format().fmt(f),
StringLikePart::TString(part) => part.format().fmt(f),
});
let part_comments = comments.leading_dangling_trailing(part);
@ -138,7 +142,7 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> {
let first_part = string.parts().next()?;
// The string is either a regular string, f-string, or bytes string.
// The string is either a regular string, f-string, t-string, or bytes string.
let normalizer = StringNormalizer::from_context(context);
// Some if a part requires preserving its quotes.
@ -164,9 +168,34 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> {
return None;
}
if let StringLikePart::FString(fstring) = part {
if context.options().target_version().supports_pep_701() {
if is_fstring_with_quoted_format_spec_and_debug(fstring, context) {
match part {
StringLikePart::FString(fstring) => {
if matches!(string, StringLike::TString(_)) {
// Don't concatenate t-strings and f-strings
return None;
}
if context.options().target_version().supports_pep_701() {
if is_interpolated_string_with_quoted_format_spec_and_debug(
&fstring.elements,
fstring.flags.into(),
context,
) {
if preserve_quotes_requirement
.is_some_and(|quote| quote != part.flags().quote_style())
{
return None;
}
preserve_quotes_requirement = Some(part.flags().quote_style());
}
}
// Avoid invalid syntax for pre Python 312:
// * When joining parts that have debug expressions with quotes: `f"{10 + len('bar')=}" f'{10 + len("bar")=}'
// * When joining parts that contain triple quoted strings with quotes: `f"{'''test ' '''}" f'{"""other " """}'`
else if is_fstring_with_quoted_debug_expression(fstring, context)
|| is_fstring_with_triple_quoted_literal_expression_containing_quotes(
fstring, context,
)
{
if preserve_quotes_requirement
.is_some_and(|quote| quote != part.flags().quote_style())
{
@ -175,21 +204,21 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> {
preserve_quotes_requirement = Some(part.flags().quote_style());
}
}
// Avoid invalid syntax for pre Python 312:
// * When joining parts that have debug expressions with quotes: `f"{10 + len('bar')=}" f'{10 + len("bar")=}'
// * When joining parts that contain triple quoted strings with quotes: `f"{'''test ' '''}" f'{"""other " """}'`
else if is_fstring_with_quoted_debug_expression(fstring, context)
|| is_fstring_with_triple_quoted_literal_expression_containing_quotes(
fstring, context,
)
{
if preserve_quotes_requirement
.is_some_and(|quote| quote != part.flags().quote_style())
{
return None;
StringLikePart::TString(tstring) => {
if is_interpolated_string_with_quoted_format_spec_and_debug(
&tstring.elements,
tstring.flags.into(),
context,
) {
if preserve_quotes_requirement
.is_some_and(|quote| quote != part.flags().quote_style())
{
return None;
}
preserve_quotes_requirement = Some(part.flags().quote_style());
}
preserve_quotes_requirement = Some(part.flags().quote_style());
}
StringLikePart::Bytes(_) | StringLikePart::String(_) => {}
}
}
@ -203,6 +232,7 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> {
StringLike::String(_) => AnyStringPrefix::Regular(StringLiteralPrefix::Empty),
StringLike::Bytes(_) => AnyStringPrefix::Bytes(ByteStringPrefix::Regular),
StringLike::FString(_) => AnyStringPrefix::Format(FStringPrefix::Regular),
StringLike::TString(_) => AnyStringPrefix::Template(TStringPrefix::Regular),
};
let quote = if let Some(quote) = preserve_quotes_requirement {
@ -287,7 +317,7 @@ impl Format<PyFormatContext<'_>> for FormatImplicitConcatenatedStringFlat<'_> {
FormatLiteralContent {
range: part.content_range(),
flags: self.flags,
is_fstring: false,
is_interpolated_string: false,
trim_start: first_non_empty && self.docstring,
trim_end: self.docstring && parts.peek().is_none(),
}
@ -300,28 +330,32 @@ impl Format<PyFormatContext<'_>> for FormatImplicitConcatenatedStringFlat<'_> {
}
}
StringLikePart::FString(f_string) => {
for element in &f_string.elements {
StringLikePart::FString(FString { elements, .. })
| StringLikePart::TString(TString { elements, .. }) => {
for element in elements {
match element {
FStringElement::Literal(literal) => {
InterpolatedStringElement::Literal(literal) => {
FormatLiteralContent {
range: literal.range(),
flags: self.flags,
is_fstring: true,
is_interpolated_string: true,
trim_end: false,
trim_start: false,
}
.fmt(f)?;
}
// Formatting the expression here and in the expanded version is safe **only**
// because we assert that the f-string never contains any comments.
FStringElement::Expression(expression) => {
let context = FStringContext::new(
// because we assert that the f/t-string never contains any comments.
InterpolatedStringElement::Interpolation(expression) => {
let context = InterpolatedStringContext::new(
self.flags,
FStringLayout::from_f_string(f_string, f.context().source()),
InterpolatedStringLayout::from_interpolated_string_elements(
elements,
f.context().source(),
),
);
FormatFStringExpressionElement::new(expression, context).fmt(f)?;
FormatInterpolatedElement::new(expression, context).fmt(f)?;
}
}
}
@ -336,7 +370,7 @@ impl Format<PyFormatContext<'_>> for FormatImplicitConcatenatedStringFlat<'_> {
struct FormatLiteralContent {
range: TextRange,
flags: AnyStringFlags,
is_fstring: bool,
is_interpolated_string: bool,
trim_start: bool,
trim_end: bool,
}
@ -348,7 +382,7 @@ impl Format<PyFormatContext<'_>> for FormatLiteralContent {
content,
0,
self.flags,
self.flags.is_f_string() && !self.is_fstring,
self.flags.is_interpolated_string() && !self.is_interpolated_string,
);
// Trim the start and end of the string if it's the first or last part of a docstring.

View file

@ -85,57 +85,55 @@ pub(crate) trait StringLikeExtensions {
impl StringLikeExtensions for ast::StringLike<'_> {
fn is_multiline(&self, context: &PyFormatContext) -> bool {
// Helper for f-string and t-string parts
fn contains_line_break_or_comments(
elements: &ast::InterpolatedStringElements,
context: &PyFormatContext,
triple_quotes: TripleQuotes,
) -> bool {
elements.iter().any(|element| match element {
ast::InterpolatedStringElement::Literal(literal) => {
triple_quotes.is_yes() && context.source().contains_line_break(literal.range())
}
ast::InterpolatedStringElement::Interpolation(expression) => {
// Expressions containing comments can't be joined.
//
// Format specifiers needs to be checked as well. For example, the
// following should be considered multiline because the literal
// part of the format specifier contains a newline at the end
// (`.3f\n`):
//
// ```py
// x = f"hello {a + b + c + d:.3f
// } world"
// ```
context.comments().contains_comments(expression.into())
|| expression.format_spec.as_deref().is_some_and(|spec| {
contains_line_break_or_comments(&spec.elements, context, triple_quotes)
})
|| expression.debug_text.as_ref().is_some_and(|debug_text| {
memchr2(b'\n', b'\r', debug_text.leading.as_bytes()).is_some()
|| memchr2(b'\n', b'\r', debug_text.trailing.as_bytes()).is_some()
})
}
})
}
self.parts().any(|part| match part {
StringLikePart::String(_) | StringLikePart::Bytes(_) => {
part.flags().is_triple_quoted()
&& context.source().contains_line_break(part.range())
}
StringLikePart::FString(f_string) => {
fn contains_line_break_or_comments(
elements: &ast::FStringElements,
context: &PyFormatContext,
triple_quotes: TripleQuotes,
) -> bool {
elements.iter().any(|element| match element {
ast::FStringElement::Literal(literal) => {
triple_quotes.is_yes()
&& context.source().contains_line_break(literal.range())
}
ast::FStringElement::Expression(expression) => {
// Expressions containing comments can't be joined.
//
// Format specifiers needs to be checked as well. For example, the
// following should be considered multiline because the literal
// part of the format specifier contains a newline at the end
// (`.3f\n`):
//
// ```py
// x = f"hello {a + b + c + d:.3f
// } world"
// ```
context.comments().contains_comments(expression.into())
|| expression.format_spec.as_deref().is_some_and(|spec| {
contains_line_break_or_comments(
&spec.elements,
context,
triple_quotes,
)
})
|| expression.debug_text.as_ref().is_some_and(|debug_text| {
memchr2(b'\n', b'\r', debug_text.leading.as_bytes()).is_some()
|| memchr2(b'\n', b'\r', debug_text.trailing.as_bytes())
.is_some()
})
}
})
}
contains_line_break_or_comments(
&f_string.elements,
context,
f_string.flags.triple_quotes(),
)
}
StringLikePart::FString(f_string) => contains_line_break_or_comments(
&f_string.elements,
context,
f_string.flags.triple_quotes(),
),
StringLikePart::TString(t_string) => contains_line_break_or_comments(
&t_string.elements,
context,
t_string.flags.triple_quotes(),
),
})
}
}

View file

@ -5,16 +5,15 @@ use std::iter::FusedIterator;
use ruff_formatter::FormatContext;
use ruff_python_ast::visitor::source_order::SourceOrderVisitor;
use ruff_python_ast::{
AnyStringFlags, BytesLiteral, FString, FStringElement, FStringElements, FStringFlags,
AnyStringFlags, BytesLiteral, FString, InterpolatedStringElement, InterpolatedStringElements,
StringFlags, StringLikePart, StringLiteral,
str::{Quote, TripleQuotes},
};
use ruff_text_size::{Ranged, TextRange, TextSlice};
use crate::QuoteStyle;
use crate::context::FStringState;
use crate::context::InterpolatedStringState;
use crate::prelude::*;
use crate::string::StringQuotes;
use crate::string::{Quote, StringQuotes, TripleQuotes};
pub(crate) struct StringNormalizer<'a, 'src> {
preferred_quote_style: Option<QuoteStyle>,
@ -47,11 +46,11 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
.unwrap_or(self.context.options().quote_style());
let supports_pep_701 = self.context.options().target_version().supports_pep_701();
// For f-strings prefer alternating the quotes unless The outer string is triple quoted and the inner isn't.
if let FStringState::InsideExpressionElement(parent_context) = self.context.f_string_state()
// For f-strings and t-strings prefer alternating the quotes unless The outer string is triple quoted and the inner isn't.
if let InterpolatedStringState::InsideInterpolatedElement(parent_context) =
self.context.interpolated_string_state()
{
let parent_flags = parent_context.f_string().flags();
let parent_flags = parent_context.interpolated_string().flags();
if !parent_flags.is_triple_quoted() || string.flags().is_triple_quoted() {
// This logic is even necessary when using preserve and the target python version doesn't support PEP701 because
// we might end up joining two f-strings that have different quote styles, in which case we need to alternate the quotes
@ -67,33 +66,49 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
return QuoteStyle::Preserve;
}
// There are cases where it is necessary to preserve the quotes to prevent an invalid f-string.
if let StringLikePart::FString(fstring) = string {
// There are two cases where it's necessary to preserve the quotes if the
// target version is pre 3.12 and the part is an f-string.
if !supports_pep_701 {
// An f-string expression contains a debug text with a quote character
// because the formatter will emit the debug expression **exactly** the
// same as in the source text.
if is_fstring_with_quoted_debug_expression(fstring, self.context) {
return QuoteStyle::Preserve;
// There are cases where it is necessary to preserve the quotes to prevent an invalid f-string or t-string.
match string {
StringLikePart::FString(fstring) => {
// There are two cases where it's necessary to preserve the quotes if the
// target version is pre 3.12 and the part is an f-string.
if !supports_pep_701 {
// An f-string expression contains a debug text with a quote character
// because the formatter will emit the debug expression **exactly** the
// same as in the source text.
if is_fstring_with_quoted_debug_expression(fstring, self.context) {
return QuoteStyle::Preserve;
}
// An f-string expression that contains a triple quoted string literal
// expression that contains a quote.
if is_fstring_with_triple_quoted_literal_expression_containing_quotes(
fstring,
self.context,
) {
return QuoteStyle::Preserve;
}
}
// An f-string expression that contains a triple quoted string literal
// expression that contains a quote.
if is_fstring_with_triple_quoted_literal_expression_containing_quotes(
fstring,
// An f-string expression element contains a debug text and the corresponding
// format specifier has a literal element with a quote character.
if is_interpolated_string_with_quoted_format_spec_and_debug(
&fstring.elements,
fstring.flags.into(),
self.context,
) {
return QuoteStyle::Preserve;
}
}
// An f-string expression element contains a debug text and the corresponding
// format specifier has a literal element with a quote character.
if is_fstring_with_quoted_format_spec_and_debug(fstring, self.context) {
return QuoteStyle::Preserve;
StringLikePart::TString(tstring) => {
if is_interpolated_string_with_quoted_format_spec_and_debug(
&tstring.elements,
tstring.flags.into(),
self.context,
) {
return QuoteStyle::Preserve;
}
}
_ => {}
}
// Per PEP 8, always prefer double quotes for triple-quoted strings.
@ -172,7 +187,7 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
// The preferred quote style is single or double quotes, and the string contains a quote or
// another character that may require escaping
(Ok(preferred_quote), Some(first_quote_or_normalized_char_offset)) => {
let metadata = if string.is_fstring() {
let metadata = if string.is_interpolated_string() {
QuoteMetadata::from_part(string, self.context, preferred_quote)
} else {
QuoteMetadata::from_str(
@ -262,9 +277,19 @@ impl QuoteMetadata {
StringLikePart::FString(fstring) => {
let metadata = QuoteMetadata::from_str("", part.flags(), preferred_quote);
metadata.merge_fstring_elements(
metadata.merge_interpolated_string_elements(
&fstring.elements,
fstring.flags,
fstring.flags.into(),
context,
preferred_quote,
)
}
StringLikePart::TString(tstring) => {
let metadata = QuoteMetadata::from_str("", part.flags(), preferred_quote);
metadata.merge_interpolated_string_elements(
&tstring.elements,
tstring.flags.into(),
context,
preferred_quote,
)
@ -369,7 +394,7 @@ impl QuoteMetadata {
})
}
/// For f-strings, only consider the quotes inside string-literals but ignore
/// For f-strings and t-strings, only consider the quotes inside string-literals but ignore
/// quotes inside expressions (except inside the format spec). This allows both the outer and the nested literals
/// to make the optimal local-choice to reduce the total number of quotes necessary.
/// This doesn't require any pre 312 special handling because an expression
@ -377,10 +402,10 @@ impl QuoteMetadata {
/// ```python
/// f"{'escaping a quote like this \" is a syntax error pre 312'}"
/// ```
fn merge_fstring_elements(
fn merge_interpolated_string_elements(
self,
elements: &FStringElements,
flags: FStringFlags,
elements: &InterpolatedStringElements,
flags: AnyStringFlags,
context: &PyFormatContext,
preferred_quote: Quote,
) -> Self {
@ -388,19 +413,19 @@ impl QuoteMetadata {
for element in elements {
match element {
FStringElement::Literal(literal) => {
InterpolatedStringElement::Literal(literal) => {
merged = merged
.merge(&QuoteMetadata::from_str(
context.source().slice(literal),
flags.into(),
flags,
preferred_quote,
))
.expect("Merge to succeed because all parts have the same flags");
}
FStringElement::Expression(expression) => {
InterpolatedStringElement::Interpolation(expression) => {
if let Some(spec) = expression.format_spec.as_deref() {
if expression.debug_text.is_none() {
merged = merged.merge_fstring_elements(
merged = merged.merge_interpolated_string_elements(
&spec.elements,
flags,
context,
@ -879,7 +904,7 @@ pub(super) fn is_fstring_with_quoted_debug_expression(
fstring: &FString,
context: &PyFormatContext,
) -> bool {
fstring.elements.expressions().any(|expression| {
fstring.elements.interpolations().any(|expression| {
if expression.debug_text.is_some() {
let content = context.source().slice(expression);
contains_opposite_quote(content, fstring.flags.into())
@ -889,58 +914,6 @@ pub(super) fn is_fstring_with_quoted_debug_expression(
})
}
/// Returns `true` if `string` has any f-string expression element (direct or nested) with a debug expression and a format spec
/// that contains the opposite quote. It's important to preserve the quote style for those f-strings
/// because changing the quote style would result in invalid syntax.
///
/// ```python
/// f'{1=: "abcd \'\'}'
/// f'{x=:a{y:"abcd"}}'
/// f'{x=:a{y:{z:"abcd"}}}'
/// ```
pub(super) fn is_fstring_with_quoted_format_spec_and_debug(
fstring: &FString,
context: &PyFormatContext,
) -> bool {
fn has_format_spec_with_opposite_quote(
elements: &FStringElements,
flags: FStringFlags,
context: &PyFormatContext,
in_debug: bool,
) -> bool {
elements.iter().any(|element| match element {
FStringElement::Literal(literal) => {
let content = context.source().slice(literal);
in_debug && contains_opposite_quote(content, flags.into())
}
FStringElement::Expression(expression) => {
expression.format_spec.as_deref().is_some_and(|spec| {
has_format_spec_with_opposite_quote(
&spec.elements,
flags,
context,
in_debug || expression.debug_text.is_some(),
)
})
}
})
}
fstring.elements.expressions().any(|expression| {
if let Some(spec) = expression.format_spec.as_deref() {
return has_format_spec_with_opposite_quote(
&spec.elements,
fstring.flags,
context,
expression.debug_text.is_some(),
);
}
false
})
}
/// Tests if the `fstring` contains any triple quoted string, byte, or f-string literal that
/// contains a quote character opposite to its own quote character.
///
@ -980,6 +953,17 @@ pub(super) fn is_fstring_with_triple_quoted_literal_expression_containing_quotes
}
}
contains_quotes
}
StringLikePart::TString(tstring) => {
let mut contains_quotes = false;
for literal in tstring.elements.literals() {
if self.contains_quote(literal.range(), tstring.flags.into()) {
contains_quotes = true;
break;
}
}
contains_quotes
}
};
@ -1018,6 +1002,59 @@ pub(super) fn is_fstring_with_triple_quoted_literal_expression_containing_quotes
visitor.found
}
/// Returns `true` if `string` has any f/t-string interpolation element (direct or nested) with a debug expression and a format spec
/// that contains the opposite quote. It's important to preserve the quote style for those f/t-strings
/// because changing the quote style would result in invalid syntax.
///
/// ```python
/// t'{1=: "abcd \'\'}'
/// t'{x=:a{y:"abcd"}}'
/// t'{x=:a{y:{z:"abcd"}}}'
/// ```
pub(super) fn is_interpolated_string_with_quoted_format_spec_and_debug(
elements: &InterpolatedStringElements,
flags: AnyStringFlags,
context: &PyFormatContext,
) -> bool {
fn has_format_spec_with_opposite_quote(
elements: &InterpolatedStringElements,
flags: AnyStringFlags,
context: &PyFormatContext,
in_debug: bool,
) -> bool {
elements.iter().any(|element| match element {
InterpolatedStringElement::Literal(literal) => {
let content = context.source().slice(literal);
in_debug && contains_opposite_quote(content, flags)
}
InterpolatedStringElement::Interpolation(expression) => {
expression.format_spec.as_deref().is_some_and(|spec| {
has_format_spec_with_opposite_quote(
&spec.elements,
flags,
context,
in_debug || expression.debug_text.is_some(),
)
})
}
})
}
elements.interpolations().any(|expression| {
if let Some(spec) = expression.format_spec.as_deref() {
return has_format_spec_with_opposite_quote(
&spec.elements,
flags,
context,
expression.debug_text.is_some(),
);
}
false
})
}
fn contains_opposite_quote(content: &str, flags: AnyStringFlags) -> bool {
if flags.is_triple_quoted() {
match flags.quote_style() {

View file

@ -6,8 +6,8 @@ use {
use ruff_python_ast::visitor::transformer::Transformer;
use ruff_python_ast::{
self as ast, BytesLiteralFlags, Expr, FStringElement, FStringFlags, FStringLiteralElement,
FStringPart, Stmt, StringFlags,
self as ast, BytesLiteralFlags, Expr, FStringFlags, FStringPart, InterpolatedStringElement,
InterpolatedStringLiteralElement, Stmt, StringFlags,
};
use ruff_python_ast::{StringLiteralFlags, visitor::transformer};
use ruff_text_size::{Ranged, TextRange};
@ -117,7 +117,7 @@ impl Transformer for Normalizer {
if can_join {
#[derive(Default)]
struct Collector {
elements: Vec<FStringElement>,
elements: Vec<InterpolatedStringElement>,
}
impl Collector {
@ -127,7 +127,7 @@ impl Transformer for Normalizer {
// `elements` vector, while subsequent strings
// are concatenated onto this top string.
fn push_literal(&mut self, literal: &str, range: TextRange) {
if let Some(FStringElement::Literal(existing_literal)) =
if let Some(InterpolatedStringElement::Literal(existing_literal)) =
self.elements.last_mut()
{
let value = std::mem::take(&mut existing_literal.value);
@ -137,8 +137,8 @@ impl Transformer for Normalizer {
existing_literal.range =
TextRange::new(existing_literal.start(), range.end());
} else {
self.elements.push(FStringElement::Literal(
FStringLiteralElement {
self.elements.push(InterpolatedStringElement::Literal(
InterpolatedStringLiteralElement {
range,
value: literal.into(),
},
@ -146,11 +146,9 @@ impl Transformer for Normalizer {
}
}
fn push_expression(
&mut self,
expression: ast::FStringExpressionElement,
) {
self.elements.push(FStringElement::Expression(expression));
fn push_expression(&mut self, expression: ast::InterpolatedElement) {
self.elements
.push(InterpolatedStringElement::Interpolation(expression));
}
}
@ -165,11 +163,13 @@ impl Transformer for Normalizer {
ast::FStringPart::FString(fstring) => {
for element in &fstring.elements {
match element {
ast::FStringElement::Literal(literal) => {
ast::InterpolatedStringElement::Literal(literal) => {
collector
.push_literal(&literal.value, literal.range);
}
ast::FStringElement::Expression(expression) => {
ast::InterpolatedStringElement::Interpolation(
expression,
) => {
collector.push_expression(expression.clone());
}
}

View file

@ -437,6 +437,19 @@ f"hellooooooooooooooooooooooo \
worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb)
aaaaaaaaaaa = f"hellooooooooooooooooooooooo \
worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb)
# This t-string should be flattened
xxxxxxxxxxxxxxxx = t"aaaaaaaaaaaaaaaaaaaaa {
expression } bbbbbbbbbbbbbbbbbbbbbbbb" + (
yyyyyyyyyyyyyy + zzzzzzzzzzz
)
# This is not a multiline t-string, but the expression is too long so it should be
# wrapped in parentheses.
t"hellooooooooooooooooooooooo \
worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb)
aaaaaaaaaaa = t"hellooooooooooooooooooooooo \
worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb)
```
## Output
@ -927,4 +940,22 @@ aaaaaaaaaaa = (
worlddddddddddddddddddddddddddddddddd"
+ (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb)
)
# This t-string should be flattened
xxxxxxxxxxxxxxxx = t"aaaaaaaaaaaaaaaaaaaaa {expression} bbbbbbbbbbbbbbbbbbbbbbbb" + (
yyyyyyyyyyyyyy + zzzzzzzzzzz
)
# This is not a multiline t-string, but the expression is too long so it should be
# wrapped in parentheses.
(
t"hellooooooooooooooooooooooo \
worlddddddddddddddddddddddddddddddddd"
+ (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb)
)
aaaaaaaaaaa = (
t"hellooooooooooooooooooooooo \
worlddddddddddddddddddddddddddddddddd"
+ (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb)
)
```

View file

@ -106,6 +106,55 @@ f"{10 + len('bar')=}" f'no debug{10}' f"{10 + len('bar')=}"
f"{10 + len('bar')=}" f'{10 + len("bar")=}'
##############################################################################
# T-strings
##############################################################################
# Escape `{` and `}` when merging a t-string with a string
"a {not_a_variable}" t"b {10}" "c"
# Join, and break expressions
t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{
expression
}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" t"cccccccccccccccccccc {20999}" "more"
# Join, but don't break the expressions
t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" t"cccccccccccccccccccc {20999}" "more"
t"test{
expression
}flat" t"can be {
joined
} together"
aaaaaaaaaaa = t"test{
expression
}flat" t"cean beeeeeeee {
joined
} eeeeeeeeeeeeeeeeeeeeeeeeeeeee" # inline
t"single quoted '{x}'" t'double quoted "{x}"' # Same number of quotes => use preferred quote style
t"single quote ' {x}" t'double quoted "{x}"' # More double quotes => use single quotes
t"single quoted '{x}'" t'double quote " {x}"' # More single quotes => use double quotes
# Different triple quoted strings
t"{'''test'''}" t'{"""other"""}'
# Now with inner quotes
t"{'''test ' '''}" t'{"""other " """}'
t"{some_where_nested('''test ' ''')}" t'{"""other " """ + "more"}'
t"{b'''test ' '''}" t'{b"""other " """}'
t"{t'''test ' '''}" t'{t"""other " """}'
# debug expressions containing quotes
t"{10 + len('bar')=}" t"{10 + len('bar')=}"
t"{10 + len('bar')=}" t'no debug{10}' t"{10 + len('bar')=}"
# We can't safely merge this pre Python 3.12 without altering the debug expression.
t"{10 + len('bar')=}" t'{10 + len("bar")=}'
##############################################################################
# Don't join raw strings
##############################################################################
@ -116,6 +165,9 @@ R"a" "normal"
f"test" fr"test"
f"test" fR"test"
t"test" tr"test"
t"test" tR"test"
##############################################################################
# Don't join triple quoted strings
@ -125,9 +177,22 @@ f"test" fR"test"
"single" f""""single"""
"single" t""""single"""
b"single" b"""triple"""
##############################################################################
# Don't join t-strings and f-strings
##############################################################################
t"{interp}" f"{expr}"
f"{expr}" t"{interp}"
f"{expr}" "string" t"{interp}"
##############################################################################
# Join strings in with statements
##############################################################################
@ -452,6 +517,50 @@ f"{10 + len('bar')=}no debug{10}{10 + len('bar')=}"
f"{10 + len('bar')=}" f'{10 + len("bar")=}'
##############################################################################
# T-strings
##############################################################################
# Escape `{` and `}` when merging a t-string with a string
t"a {{not_a_variable}}b {10}c"
# Join, and break expressions
t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{
expression
}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccc {20999}more"
# Join, but don't break the expressions
t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccc {20999}more"
t"test{expression}flatcan be {joined} together"
aaaaaaaaaaa = (
t"test{expression}flat"
t"cean beeeeeeee {joined} eeeeeeeeeeeeeeeeeeeeeeeeeeeee"
) # inline
t"single quoted '{x}'double quoted \"{x}\"" # Same number of quotes => use preferred quote style
t'single quote \' {x}double quoted "{x}"' # More double quotes => use single quotes
t"single quoted '{x}'double quote \" {x}\"" # More single quotes => use double quotes
# Different triple quoted strings
t"{'''test'''}{'''other'''}"
# Now with inner quotes
t"{'''test ' '''}{'''other " '''}"
t"{some_where_nested('''test ' ''')}{'''other " ''' + 'more'}"
t"{b'''test ' '''}{b'''other " '''}"
t"{t'''test ' '''}{t'''other " '''}"
# debug expressions containing quotes
t"{10 + len('bar')=}{10 + len('bar')=}"
t"{10 + len('bar')=}no debug{10}{10 + len('bar')=}"
# We can't safely merge this pre Python 3.12 without altering the debug expression.
t"{10 + len('bar')=}{10 + len("bar")=}"
##############################################################################
# Don't join raw strings
##############################################################################
@ -462,6 +571,9 @@ R"a" "normal"
f"test" rf"test"
f"test" Rf"test"
t"test" rt"test"
t"test" Rt"test"
##############################################################################
# Don't join triple quoted strings
@ -471,9 +583,22 @@ f"test" Rf"test"
"single" f""""single"""
"single" t""""single"""
b"single" b"""triple"""
##############################################################################
# Don't join t-strings and f-strings
##############################################################################
t"{interp}" f"{expr}"
f"{expr}" t"{interp}"
f"{expr}" "string" t"{interp}"
##############################################################################
# Join strings in with statements
##############################################################################
@ -780,7 +905,7 @@ f"aaaaaaaaaaaaaaaa \
```diff
--- Stable
+++ Preview
@@ -242,9 +242,12 @@
@@ -302,9 +302,12 @@
##############################################################################
# Use can_omit_optional_parentheses layout to avoid an instability where the formatter
# picks the can_omit_optional_parentheses layout when the strings are joined.

View file

@ -299,6 +299,155 @@ aaaaa[aaaaaaaaaaa] = (
)
#############################################################
# T-Strings
#############################################################
# Flatten and join the t-string
aaaaaaaaaaa = t"test{
expression}flat" t"cean beeeeeeee {joined} eeeeeeeeeeeeeeeee" # inline
# Parenthesize the value and join it, inline the comment
aaaaaaaaaaa = t"test{
expression}flat" t"cean beeeeeeee {joined} eeeeeeeeeeeeeeeeeeeeeeeeeee" # inline
# Parenthesize the t-string and keep it multiline because it doesn't fit on a single line including the comment
aaaaaaaaaaa = t"test{
expression
}flat" t"cean beeeeeeee {
joined
} eeeeeeeeeeeeeeeeeeeeeeeeeeeee" # inline
# The target splits because of a magic trailing comma
# The string is joined and not parenthesized because it just fits into the line length (including comment).
a[
aaaaaaa,
b,
] = t"ccccc{
expression}ccccccccccc" t"cccccccccccccccccccccccccccccccccccccccccc" # comment
# Same but starting with a joined string. They should both result in the same formatting.
[
aaaaaaa,
b,
] = t"ccccc{
expression}ccccccccccccccccccccccccccccccccccccccccccccccccccccc" # comment
# The target splits because of the magic trailing comma
# The string is **not** joined because it with the inlined comment exceeds the line length limit.
a[
aaaaaaa,
b,
] = t"ccccc{
expression}cccccccccccccccccccc" t"cccccccccccccccccccccccccccccccccccccccccc" # comment
# The target should be flat
# The string should be joined because it fits into the line length
a[
aaaaaaa,
b
] = (
t"ccccc{
expression}ccccccccccc" "cccccccccccccccccccccccc" # comment
)
# Same but starting with a joined string. They should both result in the same formatting.
a[
aaaaaaa,
b
] = t"ccccc{
expression}ccccccccccccccccccccccccccccccccccc" # comment
# The target should be flat
# The string gets parenthesized because it, with the inlined comment, exceeds the line length limit.
a[
aaaaaaa,
b
] = t"ccccc{
expression}ccccccccccc" "ccccccccccccccccccccccccccccccccccccccccccc" # comment
# Split an overlong target, but join the string if it fits
a[
aaaaaaa,
b
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
t"ccccc{
expression}ccccccccccc" "cccccccccccccccccccccccccccccc" # comment
)
# Split both if necessary and keep multiline
a[
aaaaaaa,
b
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
t"ccccc{
expression}cccccccccccccccccccccccccccccccc" "ccccccccccccccccccccccccccccccc" # comment
)
# Don't inline t-strings that contain expressions that are guaranteed to split, e.b. because of a magic trailing comma
aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
)
aaaaa[aaaaaaaaaaa] = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
aaaaa[aaaaaaaaaaa] = (t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
)
# Don't inline t-strings that contain commented expressions
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{[
a # comment
]}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{[
a # comment
]}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
# Don't inline t-strings with multiline debug expressions:
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
a=}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a +
b=}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a
=}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
a=}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a
=}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
# Trailing last-part comments
a = (
@ -380,7 +529,8 @@ self._attr_unique_id = (
return (
f"Exception in {call_back_name} when handling msg on "
f"'{msg.topic}': '{msg.payload}'" # type: ignore[str-bytes-safe]
)```
)
```
## Output
```python
@ -704,6 +854,172 @@ aaaaa[aaaaaaaaaaa] = (
)
#############################################################
# T-Strings
#############################################################
# Flatten and join the t-string
aaaaaaaaaaa = t"test{expression}flatcean beeeeeeee {joined} eeeeeeeeeeeeeeeee" # inline
# Parenthesize the value and join it, inline the comment
aaaaaaaaaaa = (
t"test{expression}flatcean beeeeeeee {joined} eeeeeeeeeeeeeeeeeeeeeeeeeee" # inline
)
# Parenthesize the t-string and keep it multiline because it doesn't fit on a single line including the comment
aaaaaaaaaaa = (
t"test{expression}flat"
t"cean beeeeeeee {joined} eeeeeeeeeeeeeeeeeeeeeeeeeeeee"
) # inline
# The target splits because of a magic trailing comma
# The string is joined and not parenthesized because it just fits into the line length (including comment).
a[
aaaaaaa,
b,
] = t"ccccc{expression}ccccccccccccccccccccccccccccccccccccccccccccccccccccc" # comment
# Same but starting with a joined string. They should both result in the same formatting.
[
aaaaaaa,
b,
] = t"ccccc{expression}ccccccccccccccccccccccccccccccccccccccccccccccccccccc" # comment
# The target splits because of the magic trailing comma
# The string is **not** joined because it with the inlined comment exceeds the line length limit.
a[
aaaaaaa,
b,
] = (
t"ccccc{expression}cccccccccccccccccccc"
t"cccccccccccccccccccccccccccccccccccccccccc"
) # comment
# The target should be flat
# The string should be joined because it fits into the line length
a[aaaaaaa, b] = t"ccccc{expression}ccccccccccccccccccccccccccccccccccc" # comment
# Same but starting with a joined string. They should both result in the same formatting.
a[aaaaaaa, b] = t"ccccc{expression}ccccccccccccccccccccccccccccccccccc" # comment
# The target should be flat
# The string gets parenthesized because it, with the inlined comment, exceeds the line length limit.
a[aaaaaaa, b] = (
t"ccccc{expression}ccccccccccc"
"ccccccccccccccccccccccccccccccccccccccccccc"
) # comment
# Split an overlong target, but join the string if it fits
a[
aaaaaaa, b
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
t"ccccc{expression}ccccccccccccccccccccccccccccccccccccccccc" # comment
)
# Split both if necessary and keep multiline
a[
aaaaaaa, b
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
t"ccccc{expression}cccccccccccccccccccccccccccccccc"
"ccccccccccccccccccccccccccccccc"
) # comment
# Don't inline t-strings that contain expressions that are guaranteed to split, e.b. because of a magic trailing comma
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[
a,
]
}"
"moreeeeeeeeeeeeeeeeeeee"
"test"
) # comment
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[
a,
]
}"
"moreeeeeeeeeeeeeeeeeeee"
"test" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[
a,
]
}"
"moreeeeeeeeeeeeeeeeeeee"
"test"
) # comment
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[
a,
]
}"
"moreeeeeeeeeeeeeeeeeeee"
"test" # comment
)
# Don't inline t-strings that contain commented expressions
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
[
a # comment
]
}"
"moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
[
a # comment
]
}"
"moreeeeeeeeeeeeeeeeeetest" # comment
)
# Don't inline t-strings with multiline debug expressions:
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
a=}"
"moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a +
b=}"
"moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a
=}"
"moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
a=}"
"moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a
=}"
"moreeeeeeeeeeeeeeeeeetest" # comment
)
# Trailing last-part comments
a = (

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,3 @@
# parse_options: {"target-version": "3.14"}
t"{}"
t"{ }"

View file

@ -0,0 +1,2 @@
# parse_options: {"target-version": "3.14"}
t"{x!z}"

View file

@ -0,0 +1,3 @@
# parse_options: {"target-version": "3.14"}
t"{x!123}"
t"{x!'a'}"

View file

@ -0,0 +1,5 @@
# parse_options: {"target-version": "3.14"}
# Starred expression inside t-string has a minimum precedence of bitwise or.
t"{*}"
t"{*x and y}"
t"{*yield x}"

View file

@ -0,0 +1,2 @@
# parse_options: {"target-version": "3.14"}
t"{lambda x: x}"

View file

@ -0,0 +1,6 @@
# parse_options: {"target-version": "3.14"}
t"{"
t"{foo!r"
t"{foo="
t"{"
t"""{"""

View file

@ -0,0 +1,3 @@
# parse_options: {"target-version": "3.14"}
t"hello {x:"
t"hello {x:.3f"

View file

@ -1,3 +1,2 @@
def foo(arg: int): ...
def foo(arg: lambda x: x): ...
def foo(arg: (x := int)): ...

View file

@ -0,0 +1,10 @@
# parse_options: {"target-version": "3.14"}
t'Magic wand: { bag['wand'] }' # nested quotes
t"{'\n'.join(a)}" # escape sequence
t'''A complex trick: {
bag['bag'] # comment
}'''
t"{t"{t"{t"{t"{t"{1+1}"}"}"}"}"}" # arbitrary nesting
t"{t'''{"nested"} inner'''} outer" # nested (triple) quotes
t"test {a \
} more" # line continuation

View file

@ -0,0 +1,74 @@
# Empty t-strings
t""
t""
t''
t""""""
t''''''
t"{" t"}"
t"{foo!s}"
t"{3,}"
t"{3!=4:}"
t'{3:{"}"}>10}'
t'{3:{"{"}>10}'
t"{ foo = }"
t"{ foo = :.3f }"
t"{ foo = !s }"
t"{ 1, 2 = }"
t'{t"{3.1415=:.1f}":*^20}'
{"foo " t"bar {x + y} " "baz": 10}
match foo:
case "one":
pass
case "implicitly " "concatenated":
pass
t"\{foo}\{bar:\}"
t"\\{{foo\\}}"
t"""{
foo:x
y
z
}"""
t"{ ( foo ) = }"
t"normal {foo} {{another}} {bar} {{{three}}}"
t"normal {foo!a} {bar!s} {baz!r} {foobar}"
t"normal {x:y + 2}"
t"{x:{{1}.pop()}}"
t"{(lambda x:{x})}"
t"{x =}"
t"{ x = }"
t"{x=!a}"
t"{x:.3f!r =}"
t"{x = !r :.3f}"
t"{x:.3f=!r}"
"hello" t"{x}"
t"{x}" t"{y}"
t"{x}" "world"
t"Invalid args in command: {command, *args}"
"foo" t"{x}" "bar"
(
t"a"
t"b"
"c"
rt"d"
fr"e"
)
# With unicode strings
u"foo" t"{bar}" "baz" " some"
"foo" t"{bar}" u"baz" " some"
"foo" t"{bar}" "baz" u" some"
u"foo" t"bar {baz} really" u"bar" "no"
# With f-strings
f"{this}" t"{that}"
t"{this}"f"{that}"
t"{this}" "that" f"{other}"
f"one {this} two" "that" t"three {other} four"
# Nesting
t"{f"{t"{this}"}"}"

Some files were not shown because too many files have changed in this diff Show more