diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S104.py b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S104.py index d7f9716ef1..9dcd08ecec 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S104.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S104.py @@ -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" diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S108.py b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S108.py index 610a6700cd..ca73cd6879 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S108.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S108.py @@ -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") diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S608.py b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S608.py index 447e46dcf2..620a18c038 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S608.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S608.py @@ -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}"}" diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.py index 12cac66d13..b3fbf5ab9e 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.py @@ -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" diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.pyi index 5cb3585c57..caf9f55e97 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.pyi @@ -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 diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/doubles_escaped.py b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/doubles_escaped.py index 7f789a22fb..92a2744f4b 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/doubles_escaped.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/doubles_escaped.py @@ -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 diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/singles_escaped.py b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/singles_escaped.py index 815db5bdb7..d68017f380 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/singles_escaped.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/singles_escaped.py @@ -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 diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_1.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_1.py index 735881301e..c29b7d53ec 100644 --- a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_1.py +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_1.py @@ -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{{}}�+-\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'=}" diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index db57ecc87f..3ca0b6ed5b 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -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; } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs index 9b9628bb03..843dd5c3b6 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs @@ -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(_) => (), } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs index 7080cb57f4..76baa9c479 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs @@ -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 { 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), diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs index 963314df64..03df078819 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs @@ -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(_) => (), } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs index 28944882b4..f558707fec 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs @@ -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 { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S104_S104.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S104_S104.py.snap index 15fec6bd70..bcad262f2f 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S104_S104.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S104_S104.py.snap @@ -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 | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S608_S608.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S608_S608.py.snap index 2170539c17..b19b9631f6 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S608_S608.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S608_S608.py.snap @@ -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}"}" + | diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs index 76ae0e1e0b..a739f91d36 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs @@ -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(), }) diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/helpers.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/helpers.rs index 7b886dd5cc..24d3a987a6 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/helpers.rs @@ -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), } } diff --git a/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs b/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs index b5b0ceadab..47972e53af 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs @@ -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. diff --git a/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs index 6d8ae3237e..2607d3d2d1 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs @@ -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; } diff --git a/crates/ruff_linter/src/rules/flake8_quotes/rules/unnecessary_escaped_quote.rs b/crates/ruff_linter/src/rules/flake8_quotes/rules/unnecessary_escaped_quote.rs index 32ea80a749..5c7d6c91d0 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/rules/unnecessary_escaped_quote.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/rules/unnecessary_escaped_quote.rs @@ -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)); } diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped.py.snap index a1099a8e0b..260805955b 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped.py.snap @@ -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 diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped_py311.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped_py311.snap index 0ca32efbb5..cf7a8b3c70 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped_py311.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped_py311.snap @@ -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 diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped.py.snap index feff195fb7..014b383162 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped.py.snap @@ -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 diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped_py311.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped_py311.snap index f6768a4b6f..6b0b2348ca 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped_py311.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped_py311.snap @@ -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 diff --git a/crates/ruff_linter/src/rules/flynt/helpers.rs b/crates/ruff_linter/src/rules/flynt/helpers.rs index a71b369b69..4afb764c4c 100644 --- a/crates/ruff_linter/src/rules/flynt/helpers.rs +++ b/crates/ruff_linter/src/rules/flynt/helpers.rs @@ -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 { +/// 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 { 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, } } diff --git a/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs b/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs index 968c386b51..aa07cb58af 100644 --- a/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs +++ b/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs @@ -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 { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/invalid_escape_sequence.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/invalid_escape_sequence.rs index d5e0107e16..1e0fe8507a 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/invalid_escape_sequence.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/invalid_escape_sequence.rs @@ -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 diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_1.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_1.py.snap index 8d763a16a3..4ca58ba4ec 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_1.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_1.py.snap @@ -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{{}}�+-\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{{}}�+-\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{{}}�+-\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{{}}�+-\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{{}}�+-\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{{}}�+-\d+" + 128 |+t"\n{{}}�+-\\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'=}" diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/f_string_missing_placeholders.rs b/crates/ruff_linter/src/rules/pyflakes/rules/f_string_missing_placeholders.rs index 4262a7baa9..3e15f4cfe3 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/f_string_missing_placeholders.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/f_string_missing_placeholders.rs @@ -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; } diff --git a/crates/ruff_linter/src/rules/pylint/rules/assert_on_string_literal.rs b/crates/ruff_linter/src/rules/pylint/rules/assert_on_string_literal.rs index 3e75254309..64db3f1e40 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/assert_on_string_literal.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/assert_on_string_literal.rs @@ -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, }) } }) { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs index 12a7798ad3..b1b888cf4c 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs @@ -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(_) diff --git a/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs b/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs index 93946dcb1f..1dd8d9c89e 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs @@ -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(_) diff --git a/crates/ruff_linter/src/rules/ruff/rules/ambiguous_unicode_character.rs b/crates/ruff_linter/src/rules/ruff/rules/ambiguous_unicode_character.rs index d1b1ef67ed..19228605b8 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/ambiguous_unicode_character.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/ambiguous_unicode_character.rs @@ -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) diff --git a/crates/ruff_linter/src/rules/ruff/rules/assert_with_print_message.rs b/crates/ruff_linter/src/rules/ruff/rules/assert_with_print_message.rs index 5c956340e1..8b3fad38dc 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/assert_with_print_message.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/assert_with_print_message.rs @@ -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 { + fn expr_to_fstring_elements(expr: &Expr) -> Vec { 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, + mut elements: impl ExactSizeIterator, flags: StringLiteralFlags, ) -> Option> { 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>, - sep: impl ExactSizeIterator, + args: impl ExactSizeIterator>, + sep: impl ExactSizeIterator, flags: StringLiteralFlags, ) -> Option { // 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>, - sep: impl ExactSizeIterator, + mut args: impl ExactSizeIterator>, + sep: impl ExactSizeIterator, flags: FStringFlags, ) -> Option { // 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 diff --git a/crates/ruff_linter/src/rules/ruff/rules/explicit_f_string_type_conversion.rs b/crates/ruff_linter/src/rules/ruff/rules/explicit_f_string_type_conversion.rs index 7318e8e683..fa88d0b083 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/explicit_f_string_type_conversion.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/explicit_f_string_type_conversion.rs @@ -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; }; diff --git a/crates/ruff_linter/src/rules/ruff/rules/invalid_formatter_suppression_comment.rs b/crates/ruff_linter/src/rules/ruff/rules/invalid_formatter_suppression_comment.rs index f5152cf1af..f691b3b9af 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/invalid_formatter_suppression_comment.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/invalid_formatter_suppression_comment.rs @@ -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, diff --git a/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs b/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs index 359b8472f8..e13cb03dd5 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs @@ -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; diff --git a/crates/ruff_python_ast/ast.toml b/crates/ruff_python_ast/ast.toml index 8d2b9e067d..38a9415515 100644 --- a/crates/ruff_python_ast/ast.toml +++ b/crates/ruff_python_ast/ast.toml @@ -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 = {} diff --git a/crates/ruff_python_ast/generate.py b/crates/ruff_python_ast/generate.py index f6afab68cc..981e23fa09 100644 --- a/crates/ruff_python_ast/generate.py +++ b/crates/ruff_python_ast/generate.py @@ -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}]" diff --git a/crates/ruff_python_ast/src/comparable.rs b/crates/ruff_python_ast/src/comparable.rs index 5197bfd38a..a759ef208a 100644 --- a/crates/ruff_python_ast/src/comparable.rs +++ b/crates/ruff_python_ast/src/comparable.rs @@ -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>>, + format_spec: Option>>, } -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> 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>, + elements: Vec>, } 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>, + interpolations: Vec>, + } + + 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>, debug_text: Option<&'a ast::DebugText>, conversion: ast::ConversionFlag, - format_spec: Vec>, + format_spec: Vec>, } #[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 { diff --git a/crates/ruff_python_ast/src/expression.rs b/crates/ruff_python_ast/src/expression.rs index 48a0342971..14fe4e1170 100644 --- a/crates/ruff_python_ast/src/expression.rs +++ b/crates/ruff_python_ast/src/expression.rs @@ -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> 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> 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> 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) diff --git a/crates/ruff_python_ast/src/generated.rs b/crates/ruff_python_ast/src/generated.rs index 166878d973..7fd7db5072 100644 --- a/crates/ruff_python_ast/src/generated.rs +++ b/crates/ruff_python_ast/src/generated.rs @@ -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 for Expr { } } +impl From for Expr { + fn from(node: crate::ExprTString) -> Self { + Self::TString(node) + } +} + impl From 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 { + 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 for FStringElement { - fn from(node: crate::FStringExpressionElement) -> Self { - Self::Expression(node) +impl From for InterpolatedStringElement { + fn from(node: crate::InterpolatedElement) -> Self { + Self::Interpolation(node) } } -impl From for FStringElement { - fn from(node: crate::FStringLiteralElement) -> Self { +impl From 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 { + pub fn interpolation(self) -> Option { 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 { + pub fn literal(self) -> Option { 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> 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> for AnyNodeRef<'a> { - fn from(node: FStringElementRef<'a>) -> AnyNodeRef<'a> { +impl<'a> From> 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> { + pub fn as_interpolated_string_element_ref(self) -> Option> { 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)] diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index 0464c636f4..ba0b08bfe6 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -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), }) } }) diff --git a/crates/ruff_python_ast/src/node.rs b/crates/ruff_python_ast/src/node.rs index 8e7a10bc52..52912c6d66 100644 --- a/crates/ruff_python_ast/src/node.rs +++ b/crates/ruff_python_ast/src/node.rs @@ -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); } } } diff --git a/crates/ruff_python_ast/src/nodes.rs b/crates/ruff_python_ast/src/nodes.rs index 40e3dd8f00..54235b2bbe 100644 --- a/crates/ruff_python_ast/src/nodes.rs +++ b/crates/ruff_python_ast/src/nodes.rs @@ -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, pub debug_text: Option, pub conversion: ConversionFlag, - pub format_spec: Option>, + pub format_spec: Option>, } /// 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, } -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 { + pub fn elements(&self) -> impl Iterator { 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) -> 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 { + 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 { + 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 { + 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 { + 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 { + 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), +} + +/// 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 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); +pub struct InterpolatedStringElements(Vec); -impl FStringElements { - /// Returns an iterator over all the [`FStringLiteralElement`] nodes contained in this f-string. - pub fn literals(&self) -> impl Iterator { +impl InterpolatedStringElements { + /// Returns an iterator over all the [`InterpolatedStringLiteralElement`] nodes contained in this f-string. + pub fn literals(&self) -> impl Iterator { 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 { - 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 { + self.iter().filter_map(|element| element.as_interpolation()) } } -impl From> for FStringElements { - fn from(elements: Vec) -> Self { - FStringElements(elements) +impl From> for InterpolatedStringElements { + fn from(elements: Vec) -> 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 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 for AnyStringFlags { impl From 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 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 for AnyStringFlags { } } +impl From 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 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), diff --git a/crates/ruff_python_ast/src/operator_precedence.rs b/crates/ruff_python_ast/src/operator_precedence.rs index 750ef7f719..6b652847e9 100644 --- a/crates/ruff_python_ast/src/operator_precedence.rs +++ b/crates/ruff_python_ast/src/operator_precedence.rs @@ -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(_) diff --git a/crates/ruff_python_ast/src/python_version.rs b/crates/ruff_python_ast/src/python_version.rs index 0658bc8dee..bc5a2689de 100644 --- a/crates/ruff_python_ast/src/python_version.rs +++ b/crates/ruff_python_ast/src/python_version.rs @@ -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 diff --git a/crates/ruff_python_ast/src/relocate.rs b/crates/ruff_python_ast/src/relocate.rs index 7410b9fc5e..c985003069 100644 --- a/crates/ruff_python_ast/src/relocate.rs +++ b/crates/ruff_python_ast/src/relocate.rs @@ -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; } diff --git a/crates/ruff_python_ast/src/str.rs b/crates/ruff_python_ast/src/str.rs index 5a8dd1093e..a9096a5218 100644 --- a/crates/ruff_python_ast/src/str.rs +++ b/crates/ruff_python_ast/src/str.rs @@ -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. `'` diff --git a/crates/ruff_python_ast/src/str_prefix.rs b/crates/ruff_python_ast/src/str_prefix.rs index 37f8421711..a00b02fb46 100644 --- a/crates/ruff_python_ast/src/str_prefix.rs +++ b/crates/ruff_python_ast/src/str_prefix.rs @@ -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(), } } } diff --git a/crates/ruff_python_ast/src/visitor.rs b/crates/ruff_python_ast/src/visitor.rs index 8bb2013efc..1e8c5ebf36 100644 --- a/crates/ruff_python_ast/src/visitor.rs +++ b/crates/ruff_python_ast/src/visitor.rs @@ -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, diff --git a/crates/ruff_python_ast/src/visitor/source_order.rs b/crates/ruff_python_ast/src/visitor/source_order.rs index 5e6ca022a0..af57ee48db 100644 --- a/crates/ruff_python_ast/src/visitor/source_order.rs +++ b/crates/ruff_python_ast/src/visitor/source_order.rs @@ -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 diff --git a/crates/ruff_python_ast/src/visitor/transformer.rs b/crates/ruff_python_ast/src/visitor/transformer.rs index f3e1c30bbc..07b098eb43 100644 --- a/crates/ruff_python_ast/src/visitor/transformer.rs +++ b/crates/ruff_python_ast/src/visitor/transformer.rs @@ -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(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( pub fn walk_f_string(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( +pub fn walk_interpolated_string_element( 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(visitor: &V, t_string: &mut TString) { + for element in &mut t_string.elements { + visitor.visit_interpolated_string_element(element); + } +} + pub fn walk_expr_context(_visitor: &V, _expr_context: &mut ExprContext) {} pub fn walk_bool_op(_visitor: &V, _bool_op: &mut BoolOp) {} diff --git a/crates/ruff_python_ast_integration_tests/tests/comparable.rs b/crates/ruff_python_ast_integration_tests/tests/comparable.rs index 89155a58a0..8f792f1bdd 100644 --- a/crates/ruff_python_ast_integration_tests/tests/comparable.rs +++ b/crates/ruff_python_ast_integration_tests/tests/comparable.rs @@ -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) } diff --git a/crates/ruff_python_ast_integration_tests/tests/snapshots/source_order__f_strings.snap b/crates/ruff_python_ast_integration_tests/tests/snapshots/source_order__f_strings.snap index 84266e3745..87e9af87a6 100644 --- a/crates/ruff_python_ast_integration_tests/tests/snapshots/source_order__f_strings.snap +++ b/crates/ruff_python_ast_integration_tests/tests/snapshots/source_order__f_strings.snap @@ -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 diff --git a/crates/ruff_python_ast_integration_tests/tests/snapshots/source_order__t_strings.snap b/crates/ruff_python_ast_integration_tests/tests/snapshots/source_order__t_strings.snap new file mode 100644 index 0000000000..75e4f537b2 --- /dev/null +++ b/crates/ruff_python_ast_integration_tests/tests/snapshots/source_order__t_strings.snap @@ -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 diff --git a/crates/ruff_python_ast_integration_tests/tests/snapshots/visitor__f_strings.snap b/crates/ruff_python_ast_integration_tests/tests/snapshots/visitor__f_strings.snap index f379b791d7..4d81357f9e 100644 --- a/crates/ruff_python_ast_integration_tests/tests/snapshots/visitor__f_strings.snap +++ b/crates/ruff_python_ast_integration_tests/tests/snapshots/visitor__f_strings.snap @@ -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 diff --git a/crates/ruff_python_ast_integration_tests/tests/snapshots/visitor__t_strings.snap b/crates/ruff_python_ast_integration_tests/tests/snapshots/visitor__t_strings.snap new file mode 100644 index 0000000000..58def387aa --- /dev/null +++ b/crates/ruff_python_ast_integration_tests/tests/snapshots/visitor__t_strings.snap @@ -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 diff --git a/crates/ruff_python_ast_integration_tests/tests/source_order.rs b/crates/ruff_python_ast_integration_tests/tests/source_order.rs index 1f01d91c7f..ea257eb279 100644 --- a/crates/ruff_python_ast_integration_tests/tests/source_order.rs +++ b/crates/ruff_python_ast_integration_tests/tests/source_order.rs @@ -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(); diff --git a/crates/ruff_python_ast_integration_tests/tests/visitor.rs b/crates/ruff_python_ast_integration_tests/tests/visitor.rs index c99365898b..9cdc7d998a 100644 --- a/crates/ruff_python_ast_integration_tests/tests/visitor.rs +++ b/crates/ruff_python_ast_integration_tests/tests/visitor.rs @@ -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(); } } diff --git a/crates/ruff_python_codegen/src/generator.rs b/crates/ruff_python_codegen/src/generator.rs index d435ff5c22..8933205731 100644 --- a/crates/ruff_python_codegen/src/generator.rs +++ b/crates/ruff_python_codegen/src/generator.rs @@ -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 { diff --git a/crates/ruff_python_formatter/generate.py b/crates/ruff_python_formatter/generate.py index ba8354c2aa..ded6ae5b00 100755 --- a/crates/ruff_python_formatter/generate.py +++ b/crates/ruff_python_formatter/generate.py @@ -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 diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py index 8b27e3cd9f..439d0f1cc1 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py @@ -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) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string.py index ede789e997..e945211bec 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string.py @@ -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 ############################################################################## diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string_assignment.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string_assignment.py index 642ad83c27..c70233ea52 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string_assignment.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string_assignment.py @@ -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] -) \ No newline at end of file +) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.options.json new file mode 100644 index 0000000000..c485014b9b --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.options.json @@ -0,0 +1 @@ +[{"target_version": "3.14"}] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.py new file mode 100644 index 0000000000..31087f1610 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.py @@ -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, }") diff --git a/crates/ruff_python_formatter/src/builders.rs b/crates/ruff_python_formatter/src/builders.rs index 6d7c79e33d..8d7aeb502b 100644 --- a/crates/ruff_python_formatter/src/builders.rs +++ b/crates/ruff_python_formatter/src/builders.rs @@ -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) { diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index 43ebbfa68e..23cc3ee996 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -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(_) diff --git a/crates/ruff_python_formatter/src/context.rs b/crates/ruff_python_formatter/src/context.rs index 946deaf1a6..b1b3a7941a 100644 --- a/crates/ruff_python_formatter/src/context.rs +++ b/crates/ruff_python_formatter/src/context.rs @@ -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, - /// 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 { 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, B: Buffer>, { 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, B: Buffer>, { - 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, B: Buffer>, @@ -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, B: Buffer>, @@ -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, B: Buffer>, @@ -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); } } diff --git a/crates/ruff_python_formatter/src/expression/expr_f_string.rs b/crates/ruff_python_formatter/src/expression/expr_f_string.rs index 90db025694..ad559e102a 100644 --- a/crates/ruff_python_formatter/src/expression/expr_f_string.rs +++ b/crates/ruff_python_formatter/src/expression/expr_f_string.rs @@ -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 { diff --git a/crates/ruff_python_formatter/src/expression/expr_t_string.rs b/crates/ruff_python_formatter/src/expression/expr_t_string.rs new file mode 100644 index 0000000000..d937338baf --- /dev/null +++ b/crates/ruff_python_formatter/src/expression/expr_t_string.rs @@ -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 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 + } + } +} diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index aa5be13813..025c4e7513 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -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> 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(_) diff --git a/crates/ruff_python_formatter/src/generated.rs b/crates/ruff_python_formatter/src/generated.rs index 9bb79d80ac..3abc77a538 100644 --- a/crates/ruff_python_formatter/src/generated.rs +++ b/crates/ruff_python_formatter/src/generated.rs @@ -1562,6 +1562,42 @@ impl<'ast> IntoFormat> for ast::ExprFString { } } +impl FormatRule> + for crate::expression::expr_t_string::FormatExprTString +{ + #[inline] + fn fmt(&self, node: &ast::ExprTString, f: &mut PyFormatter) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl<'ast> AsFormat> 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> 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> for crate::expression::expr_string_literal::FormatExprStringLiteral { @@ -2963,6 +2999,34 @@ impl<'ast> IntoFormat> for ast::FString { } } +impl FormatRule> for crate::other::t_string::FormatTString { + #[inline] + fn fmt(&self, node: &ast::TString, f: &mut PyFormatter) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl<'ast> AsFormat> 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> 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> for crate::other::string_literal::FormatStringLiteral { diff --git a/crates/ruff_python_formatter/src/other/f_string.rs b/crates/ruff_python_formatter/src/other/f_string.rs index c423f39f34..a02cd4a086 100644 --- a/crates/ruff_python_formatter/src/other/f_string.rs +++ b/crates/ruff_python_formatter/src/other/f_string.rs @@ -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 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 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) - } -} diff --git a/crates/ruff_python_formatter/src/other/interpolated_string.rs b/crates/ruff_python_formatter/src/other/interpolated_string.rs new file mode 100644 index 0000000000..7a0c8b3c1c --- /dev/null +++ b/crates/ruff_python_formatter/src/other/interpolated_string.rs @@ -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) + } +} diff --git a/crates/ruff_python_formatter/src/other/f_string_element.rs b/crates/ruff_python_formatter/src/other/interpolated_string_element.rs similarity index 80% rename from crates/ruff_python_formatter/src/other/f_string_element.rs rename to crates/ruff_python_formatter/src/other/interpolated_string_element.rs index 8418d5edf0..e0b53331ea 100644 --- a/crates/ruff_python_formatter/src/other/f_string_element.rs +++ b/crates/ruff_python_formatter/src/other/interpolated_string_element.rs @@ -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> for FormatFStringElement<'_> { +impl Format> 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> 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> 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> for FormatFStringExpressionElement<'_> { +impl Format> for FormatInterpolatedElement<'_> { fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { - let FStringExpressionElement { + let InterpolatedElement { expression, debug_text, conversion, @@ -214,8 +225,8 @@ impl Format> 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> 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 diff --git a/crates/ruff_python_formatter/src/other/mod.rs b/crates/ruff_python_formatter/src/other/mod.rs index b55b1a70f6..33f14c4f7b 100644 --- a/crates/ruff_python_formatter/src/other/mod.rs +++ b/crates/ruff_python_formatter/src/other/mod.rs @@ -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; diff --git a/crates/ruff_python_formatter/src/other/t_string.rs b/crates/ruff_python_formatter/src/other/t_string.rs new file mode 100644 index 0000000000..098c668c51 --- /dev/null +++ b/crates/ruff_python_formatter/src/other/t_string.rs @@ -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 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) + } +} diff --git a/crates/ruff_python_formatter/src/pattern/mod.rs b/crates/ruff_python_formatter/src/pattern/mod.rs index fa197378a1..e255d59359 100644 --- a/crates/ruff_python_formatter/src/pattern/mod.rs +++ b/crates/ruff_python_formatter/src/pattern/mod.rs @@ -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 ); } diff --git a/crates/ruff_python_formatter/src/range.rs b/crates/ruff_python_formatter/src/range.rs index 2f2f12644a..61043061b5 100644 --- a/crates/ruff_python_formatter/src/range.rs +++ b/crates/ruff_python_formatter/src/range.rs @@ -659,10 +659,11 @@ impl Format> 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> for FormatEnclosingNode<'_> { | AnyNodeRef::ExprIpyEscapeCommand(_) | AnyNodeRef::FString(_) | AnyNodeRef::StringLiteral(_) + | AnyNodeRef::TString(_) | AnyNodeRef::PatternMatchValue(_) | AnyNodeRef::PatternMatchSingleton(_) | AnyNodeRef::PatternMatchSequence(_) diff --git a/crates/ruff_python_formatter/src/statement/stmt_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_assign.rs index 4aad6a47a2..54a2eec8bb 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_assign.rs @@ -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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> for FormatStatementsLastExpression<'_> { space(), operator, space(), - format_f_string, + format_interpolated_string, ] ) }); @@ -1016,7 +1033,7 @@ impl Format> 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> 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> 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> 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> 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> { + 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, } } diff --git a/crates/ruff_python_formatter/src/string/implicit.rs b/crates/ruff_python_formatter/src/string/implicit.rs index fd6145c033..fc25d030aa 100644 --- a/crates/ruff_python_formatter/src/string/implicit.rs +++ b/crates/ruff_python_formatter/src/string/implicit.rs @@ -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> 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> 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> 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> 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> 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. diff --git a/crates/ruff_python_formatter/src/string/mod.rs b/crates/ruff_python_formatter/src/string/mod.rs index 52fd92ad80..6f9fc5b33e 100644 --- a/crates/ruff_python_formatter/src/string/mod.rs +++ b/crates/ruff_python_formatter/src/string/mod.rs @@ -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(), + ), }) } } diff --git a/crates/ruff_python_formatter/src/string/normalize.rs b/crates/ruff_python_formatter/src/string/normalize.rs index b8f2902d85..1e694a66ff 100644 --- a/crates/ruff_python_formatter/src/string/normalize.rs +++ b/crates/ruff_python_formatter/src/string/normalize.rs @@ -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, @@ -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() { diff --git a/crates/ruff_python_formatter/tests/normalizer.rs b/crates/ruff_python_formatter/tests/normalizer.rs index 2786082cfe..8bed90221c 100644 --- a/crates/ruff_python_formatter/tests/normalizer.rs +++ b/crates/ruff_python_formatter/tests/normalizer.rs @@ -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, + elements: Vec, } 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()); } } diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap index 9a5b9d1882..a5f3462811 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap @@ -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) +) ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string.py.snap index c2e7f51ca1..6c577df6bb 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string.py.snap @@ -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. diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string_assignment.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string_assignment.py.snap index 7133eb0b8c..c5237dcb54 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string_assignment.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string_assignment.py.snap @@ -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 = ( diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__tstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__tstring.py.snap new file mode 100644 index 0000000000..5c08140e3e --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__tstring.py.snap @@ -0,0 +1,1536 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.py +--- +## Input +```python +( + 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, }") +``` + +## Outputs +### Output 1 +``` +indent-style = space +line-width = 88 +indent-width = 4 +quote-style = Double +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Disabled +docstring-code-line-width = "dynamic" +preview = Disabled +target_version = 3.14 +source_type = Python +``` + +```python +(t"{one}{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}{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 + +rf"single quotes ' {x}" # Keep double because `'` can't be escaped +rf'double quotes " {x}' # Keep single because `"` can't be escaped +rf"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}flatcan 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"{'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' if True else ''}" +t"{'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 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 }") +``` diff --git a/crates/ruff_python_parser/resources/inline/err/t_string_empty_expression.py b/crates/ruff_python_parser/resources/inline/err/t_string_empty_expression.py new file mode 100644 index 0000000000..257a0e1209 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/t_string_empty_expression.py @@ -0,0 +1,3 @@ +# parse_options: {"target-version": "3.14"} +t"{}" +t"{ }" diff --git a/crates/ruff_python_parser/resources/inline/err/t_string_invalid_conversion_flag_name_tok.py b/crates/ruff_python_parser/resources/inline/err/t_string_invalid_conversion_flag_name_tok.py new file mode 100644 index 0000000000..dcea20f590 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/t_string_invalid_conversion_flag_name_tok.py @@ -0,0 +1,2 @@ +# parse_options: {"target-version": "3.14"} +t"{x!z}" diff --git a/crates/ruff_python_parser/resources/inline/err/t_string_invalid_conversion_flag_other_tok.py b/crates/ruff_python_parser/resources/inline/err/t_string_invalid_conversion_flag_other_tok.py new file mode 100644 index 0000000000..61fb5815ba --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/t_string_invalid_conversion_flag_other_tok.py @@ -0,0 +1,3 @@ +# parse_options: {"target-version": "3.14"} +t"{x!123}" +t"{x!'a'}" diff --git a/crates/ruff_python_parser/resources/inline/err/t_string_invalid_starred_expr.py b/crates/ruff_python_parser/resources/inline/err/t_string_invalid_starred_expr.py new file mode 100644 index 0000000000..77bf4eb55f --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/t_string_invalid_starred_expr.py @@ -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}" diff --git a/crates/ruff_python_parser/resources/inline/err/t_string_lambda_without_parentheses.py b/crates/ruff_python_parser/resources/inline/err/t_string_lambda_without_parentheses.py new file mode 100644 index 0000000000..0d9e70011c --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/t_string_lambda_without_parentheses.py @@ -0,0 +1,2 @@ +# parse_options: {"target-version": "3.14"} +t"{lambda x: x}" diff --git a/crates/ruff_python_parser/resources/inline/err/t_string_unclosed_lbrace.py b/crates/ruff_python_parser/resources/inline/err/t_string_unclosed_lbrace.py new file mode 100644 index 0000000000..b943b533e5 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/t_string_unclosed_lbrace.py @@ -0,0 +1,6 @@ +# parse_options: {"target-version": "3.14"} +t"{" +t"{foo!r" +t"{foo=" +t"{" +t"""{""" diff --git a/crates/ruff_python_parser/resources/inline/err/t_string_unclosed_lbrace_in_format_spec.py b/crates/ruff_python_parser/resources/inline/err/t_string_unclosed_lbrace_in_format_spec.py new file mode 100644 index 0000000000..cced3bb064 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/t_string_unclosed_lbrace_in_format_spec.py @@ -0,0 +1,3 @@ +# parse_options: {"target-version": "3.14"} +t"hello {x:" +t"hello {x:.3f" diff --git a/crates/ruff_python_parser/resources/inline/ok/param_with_annotation.py b/crates/ruff_python_parser/resources/inline/ok/param_with_annotation.py index e109ecaebc..c8c0425ce6 100644 --- a/crates/ruff_python_parser/resources/inline/ok/param_with_annotation.py +++ b/crates/ruff_python_parser/resources/inline/ok/param_with_annotation.py @@ -1,3 +1,2 @@ def foo(arg: int): ... def foo(arg: lambda x: x): ... -def foo(arg: (x := int)): ... diff --git a/crates/ruff_python_parser/resources/inline/ok/pep750_t_string_py314.py b/crates/ruff_python_parser/resources/inline/ok/pep750_t_string_py314.py new file mode 100644 index 0000000000..18cfc9a08f --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/pep750_t_string_py314.py @@ -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 diff --git a/crates/ruff_python_parser/resources/valid/expressions/t_string.py b/crates/ruff_python_parser/resources/valid/expressions/t_string.py new file mode 100644 index 0000000000..0bb6278492 --- /dev/null +++ b/crates/ruff_python_parser/resources/valid/expressions/t_string.py @@ -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}"}"}" diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index 2620f67c77..b7bacffb57 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -3,7 +3,7 @@ use std::fmt::{self, Display}; use ruff_python_ast::PythonVersion; use ruff_text_size::{Ranged, TextRange}; -use crate::TokenKind; +use crate::{TokenKind, string::InterpolatedStringKind}; /// Represents represent errors that occur during parsing and are /// returned by the `parse_*` functions. @@ -48,9 +48,9 @@ impl ParseError { } } -/// Represents the different types of errors that can occur during parsing of an f-string. +/// Represents the different types of errors that can occur during parsing of an f-string or t-string. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum FStringErrorType { +pub enum InterpolatedStringErrorType { /// Expected a right brace after an opened left brace. UnclosedLbrace, /// An invalid conversion flag was encountered. @@ -65,9 +65,9 @@ pub enum FStringErrorType { LambdaWithoutParentheses, } -impl std::fmt::Display for FStringErrorType { +impl std::fmt::Display for InterpolatedStringErrorType { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - use FStringErrorType::{ + use InterpolatedStringErrorType::{ InvalidConversionFlag, LambdaWithoutParentheses, SingleRbrace, UnclosedLbrace, UnterminatedString, UnterminatedTripleQuotedString, }; @@ -177,12 +177,26 @@ pub enum ParseErrorType { /// An unexpected token was found at the end of an expression parsing UnexpectedExpressionToken, - /// An f-string error containing the [`FStringErrorType`]. - FStringError(FStringErrorType), + /// An f-string error containing the [`InterpolatedStringErrorType`]. + FStringError(InterpolatedStringErrorType), + /// A t-string error containing the [`InterpolatedStringErrorType`]. + TStringError(InterpolatedStringErrorType), /// Parser encountered an error during lexing. Lexical(LexicalErrorType), } +impl ParseErrorType { + pub(crate) fn from_interpolated_string_error( + error: InterpolatedStringErrorType, + string_kind: InterpolatedStringKind, + ) -> Self { + match string_kind { + InterpolatedStringKind::FString => Self::FStringError(error), + InterpolatedStringKind::TString => Self::TStringError(error), + } + } +} + impl std::error::Error for ParseErrorType {} impl std::fmt::Display for ParseErrorType { @@ -292,6 +306,9 @@ impl std::fmt::Display for ParseErrorType { ParseErrorType::FStringError(fstring_error) => { write!(f, "f-string: {fstring_error}") } + ParseErrorType::TStringError(tstring_error) => { + write!(f, "t-string: {tstring_error}") + } ParseErrorType::UnexpectedExpressionToken => { write!(f, "Unexpected token at the end of an expression") } @@ -375,8 +392,10 @@ pub enum LexicalErrorType { IndentationError, /// An unrecognized token was encountered. UnrecognizedToken { tok: char }, - /// An f-string error containing the [`FStringErrorType`]. - FStringError(FStringErrorType), + /// An f-string error containing the [`InterpolatedStringErrorType`]. + FStringError(InterpolatedStringErrorType), + /// A t-string error containing the [`InterpolatedStringErrorType`]. + TStringError(InterpolatedStringErrorType), /// Invalid character encountered in a byte literal. InvalidByteLiteral, /// An unexpected character was encountered after a line continuation. @@ -389,11 +408,24 @@ pub enum LexicalErrorType { impl std::error::Error for LexicalErrorType {} +impl LexicalErrorType { + pub(crate) fn from_interpolated_string_error( + error: InterpolatedStringErrorType, + string_kind: InterpolatedStringKind, + ) -> Self { + match string_kind { + InterpolatedStringKind::FString => Self::FStringError(error), + InterpolatedStringKind::TString => Self::TStringError(error), + } + } +} + impl std::fmt::Display for LexicalErrorType { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { LexicalErrorType::StringError => write!(f, "Got unexpected string"), LexicalErrorType::FStringError(error) => write!(f, "f-string: {error}"), + LexicalErrorType::TStringError(error) => write!(f, "t-string: {error}"), LexicalErrorType::InvalidByteLiteral => { write!(f, "bytes can only contain ASCII literal characters") } @@ -848,6 +880,12 @@ pub enum UnsupportedSyntaxErrorKind { /// /// [PEP 758]: https://peps.python.org/pep-0758/ UnparenthesizedExceptionTypes, + /// Represents the use of a template string (t-string) + /// literal prior to the implementation of [PEP 750] + /// in Python 3.14. + /// + /// [PEP 750]: https://peps.python.org/pep-0750/ + TemplateStrings, } impl Display for UnsupportedSyntaxError { @@ -928,6 +966,7 @@ impl Display for UnsupportedSyntaxError { UnsupportedSyntaxErrorKind::UnparenthesizedExceptionTypes => { "Multiple exception types must be parenthesized" } + UnsupportedSyntaxErrorKind::TemplateStrings => "Cannot use t-strings", }; write!( @@ -998,6 +1037,7 @@ impl UnsupportedSyntaxErrorKind { UnsupportedSyntaxErrorKind::UnparenthesizedExceptionTypes => { Change::Added(PythonVersion::PY314) } + UnsupportedSyntaxErrorKind::TemplateStrings => Change::Added(PythonVersion::PY314), } } diff --git a/crates/ruff_python_parser/src/lexer.rs b/crates/ruff_python_parser/src/lexer.rs index 9e3d47f890..d04f377678 100644 --- a/crates/ruff_python_parser/src/lexer.rs +++ b/crates/ruff_python_parser/src/lexer.rs @@ -18,15 +18,17 @@ use ruff_python_trivia::is_python_whitespace; use ruff_text_size::{TextLen, TextRange, TextSize}; use crate::Mode; -use crate::error::{FStringErrorType, LexicalError, LexicalErrorType}; +use crate::error::{InterpolatedStringErrorType, LexicalError, LexicalErrorType}; use crate::lexer::cursor::{Cursor, EOF_CHAR}; -use crate::lexer::fstring::{FStringContext, FStrings, FStringsCheckpoint}; use crate::lexer::indentation::{Indentation, Indentations, IndentationsCheckpoint}; +use crate::lexer::interpolated_string::{ + InterpolatedStringContext, InterpolatedStrings, InterpolatedStringsCheckpoint, +}; use crate::token::{TokenFlags, TokenKind, TokenValue}; mod cursor; -mod fstring; mod indentation; +mod interpolated_string; const BOM: char = '\u{feff}'; @@ -65,8 +67,8 @@ pub struct Lexer<'src> { /// Lexer mode. mode: Mode, - /// F-string contexts. - fstrings: FStrings, + /// F-string and t-string contexts. + interpolated_strings: InterpolatedStrings, /// Errors encountered while lexing. errors: Vec, @@ -102,7 +104,7 @@ impl<'src> Lexer<'src> { indentations: Indentations::default(), pending_indentation: None, mode, - fstrings: FStrings::default(), + interpolated_strings: InterpolatedStrings::default(), errors: Vec::new(), }; @@ -162,11 +164,11 @@ impl<'src> Lexer<'src> { } fn lex_token(&mut self) -> TokenKind { - if let Some(fstring) = self.fstrings.current() { - if !fstring.is_in_expression(self.nesting) { - if let Some(token) = self.lex_fstring_middle_or_end() { - if matches!(token, TokenKind::FStringEnd) { - self.fstrings.pop(); + if let Some(interpolated_string) = self.interpolated_strings.current() { + if !interpolated_string.is_in_interpolation(self.nesting) { + if let Some(token) = self.lex_interpolated_string_middle_or_end() { + if token.is_interpolated_string_end() { + self.interpolated_strings.pop(); } return token; } @@ -506,23 +508,26 @@ impl<'src> Lexer<'src> { TokenKind::Lbrace } '}' => { - if let Some(fstring) = self.fstrings.current_mut() { - if fstring.nesting() == self.nesting { - return self.push_error(LexicalError::new( - LexicalErrorType::FStringError(FStringErrorType::SingleRbrace), - self.token_range(), - )); + if let Some(interpolated_string) = self.interpolated_strings.current_mut() { + if interpolated_string.nesting() == self.nesting { + let error_type = LexicalErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::SingleRbrace, + interpolated_string.kind(), + ); + return self.push_error(LexicalError::new(error_type, self.token_range())); } - fstring.try_end_format_spec(self.nesting); + interpolated_string.try_end_format_spec(self.nesting); } self.nesting = self.nesting.saturating_sub(1); TokenKind::Rbrace } ':' => { if self - .fstrings + .interpolated_strings .current_mut() - .is_some_and(|fstring| fstring.try_start_format_spec(self.nesting)) + .is_some_and(|interpolated_string| { + interpolated_string.try_start_format_spec(self.nesting) + }) { TokenKind::Colon } else if self.cursor.eat_char('=') { @@ -573,8 +578,8 @@ impl<'src> Lexer<'src> { self.state = State::AfterNewline; TokenKind::Newline } else { - if let Some(fstring) = self.fstrings.current_mut() { - fstring.try_end_format_spec(self.nesting); + if let Some(interpolated_string) = self.interpolated_strings.current_mut() { + interpolated_string.try_end_format_spec(self.nesting); } TokenKind::NonLogicalNewline }; @@ -586,8 +591,8 @@ impl<'src> Lexer<'src> { self.state = State::AfterNewline; TokenKind::Newline } else { - if let Some(fstring) = self.fstrings.current_mut() { - fstring.try_end_format_spec(self.nesting); + if let Some(interpolated_string) = self.interpolated_strings.current_mut() { + interpolated_string.try_end_format_spec(self.nesting); } TokenKind::NonLogicalNewline }; @@ -610,7 +615,7 @@ impl<'src> Lexer<'src> { /// Lex an identifier. Also used for keywords and string/bytes literals with a prefix. fn lex_identifier(&mut self, first: char) -> TokenKind { - // Detect potential string like rb'' b'' f'' u'' r'' + // Detect potential string like rb'' b'' f'' t'' u'' r'' let quote = match (first, self.cursor.first()) { (_, quote @ ('\'' | '"')) => self.try_single_char_prefix(first).then(|| { self.cursor.bump(); @@ -627,8 +632,10 @@ impl<'src> Lexer<'src> { }; if let Some(quote) = quote { - if self.current_flags.is_f_string() { - return self.lex_fstring_start(quote); + if self.current_flags.is_interpolated_string() { + if let Some(kind) = self.lex_interpolated_string_start(quote) { + return kind; + } } return self.lex_string(quote); @@ -711,6 +718,7 @@ impl<'src> Lexer<'src> { fn try_single_char_prefix(&mut self, first: char) -> bool { match first { 'f' | 'F' => self.current_flags |= TokenFlags::F_STRING, + 't' | 'T' => self.current_flags |= TokenFlags::T_STRING, 'u' | 'U' => self.current_flags |= TokenFlags::UNICODE_STRING, 'b' | 'B' => self.current_flags |= TokenFlags::BYTE_STRING, 'r' => self.current_flags |= TokenFlags::RAW_STRING_LOWERCASE, @@ -730,6 +738,12 @@ impl<'src> Lexer<'src> { ['R', 'f' | 'F'] | ['f' | 'F', 'R'] => { self.current_flags |= TokenFlags::F_STRING | TokenFlags::RAW_STRING_UPPERCASE; } + ['r', 't' | 'T'] | ['t' | 'T', 'r'] => { + self.current_flags |= TokenFlags::T_STRING | TokenFlags::RAW_STRING_LOWERCASE; + } + ['R', 't' | 'T'] | ['t' | 'T', 'R'] => { + self.current_flags |= TokenFlags::T_STRING | TokenFlags::RAW_STRING_UPPERCASE; + } ['r', 'b' | 'B'] | ['b' | 'B', 'r'] => { self.current_flags |= TokenFlags::BYTE_STRING | TokenFlags::RAW_STRING_LOWERCASE; } @@ -741,8 +755,8 @@ impl<'src> Lexer<'src> { true } - /// Lex a f-string start token. - fn lex_fstring_start(&mut self, quote: char) -> TokenKind { + /// Lex a f-string or t-string start token if positioned at the start of an f-string or t-string. + fn lex_interpolated_string_start(&mut self, quote: char) -> Option { #[cfg(debug_assertions)] debug_assert_eq!(self.cursor.previous(), quote); @@ -754,27 +768,31 @@ impl<'src> Lexer<'src> { self.current_flags |= TokenFlags::TRIPLE_QUOTED_STRING; } - self.fstrings - .push(FStringContext::new(self.current_flags, self.nesting)); + let ftcontext = InterpolatedStringContext::new(self.current_flags, self.nesting)?; - TokenKind::FStringStart + let kind = ftcontext.kind(); + + self.interpolated_strings.push(ftcontext); + + Some(kind.start_token()) } - /// Lex a f-string middle or end token. - fn lex_fstring_middle_or_end(&mut self) -> Option { + /// Lex an f-string or t-string middle or end token. + fn lex_interpolated_string_middle_or_end(&mut self) -> Option { // SAFETY: Safe because the function is only called when `self.fstrings` is not empty. - let fstring = self.fstrings.current().unwrap(); + let interpolated_string = self.interpolated_strings.current().unwrap(); + let string_kind = interpolated_string.kind(); // Check if we're at the end of the f-string. - if fstring.is_triple_quoted() { - let quote_char = fstring.quote_char(); + if interpolated_string.is_triple_quoted() { + let quote_char = interpolated_string.quote_char(); if self.cursor.eat_char3(quote_char, quote_char, quote_char) { - self.current_flags = fstring.flags(); - return Some(TokenKind::FStringEnd); + self.current_flags = interpolated_string.flags(); + return Some(string_kind.end_token()); } - } else if self.cursor.eat_char(fstring.quote_char()) { - self.current_flags = fstring.flags(); - return Some(TokenKind::FStringEnd); + } else if self.cursor.eat_char(interpolated_string.quote_char()) { + self.current_flags = interpolated_string.flags(); + return Some(string_kind.end_token()); } // We have to decode `{{` and `}}` into `{` and `}` respectively. As an @@ -786,7 +804,7 @@ impl<'src> Lexer<'src> { let mut last_offset = self.offset(); // This isn't going to change for the duration of the loop. - let in_format_spec = fstring.is_in_format_spec(self.nesting); + let in_format_spec = interpolated_string.is_in_format_spec(self.nesting); let mut in_named_unicode = false; @@ -796,18 +814,18 @@ impl<'src> Lexer<'src> { // in the source code and the one returned by `self.cursor.first()` when // we reach the end of the source code. EOF_CHAR if self.cursor.is_eof() => { - let error = if fstring.is_triple_quoted() { - FStringErrorType::UnterminatedTripleQuotedString + let error = if interpolated_string.is_triple_quoted() { + InterpolatedStringErrorType::UnterminatedTripleQuotedString } else { - FStringErrorType::UnterminatedString + InterpolatedStringErrorType::UnterminatedString }; - self.fstrings.pop(); + self.interpolated_strings.pop(); return Some(self.push_error(LexicalError::new( - LexicalErrorType::FStringError(error), + LexicalErrorType::from_interpolated_string_error(error, string_kind), self.token_range(), ))); } - '\n' | '\r' if !fstring.is_triple_quoted() => { + '\n' | '\r' if !interpolated_string.is_triple_quoted() => { // If we encounter a newline while we're in a format spec, then // we stop here and let the lexer emit the newline token. // @@ -815,9 +833,12 @@ impl<'src> Lexer<'src> { if in_format_spec { break; } - self.fstrings.pop(); + self.interpolated_strings.pop(); return Some(self.push_error(LexicalError::new( - LexicalErrorType::FStringError(FStringErrorType::UnterminatedString), + LexicalErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::UnterminatedString, + string_kind, + ), self.token_range(), ))); } @@ -827,7 +848,7 @@ impl<'src> Lexer<'src> { // Don't consume `{` or `}` as we want them to be emitted as tokens. // They will be handled in the next iteration. continue; - } else if !fstring.is_raw_string() { + } else if !interpolated_string.is_raw_string() { if self.cursor.eat_char2('N', '{') { in_named_unicode = true; continue; @@ -840,8 +861,8 @@ impl<'src> Lexer<'src> { self.cursor.bump(); } } - quote @ ('\'' | '"') if quote == fstring.quote_char() => { - if let Some(triple_quotes) = fstring.triple_quotes() { + quote @ ('\'' | '"') if quote == interpolated_string.quote_char() => { + if let Some(triple_quotes) = interpolated_string.triple_quotes() { if self.cursor.rest().starts_with(triple_quotes) { break; } @@ -892,10 +913,10 @@ impl<'src> Lexer<'src> { normalized }; - self.current_value = TokenValue::FStringMiddle(value.into_boxed_str()); + self.current_value = TokenValue::InterpolatedStringMiddle(value.into_boxed_str()); - self.current_flags = fstring.flags(); - Some(TokenKind::FStringMiddle) + self.current_flags = interpolated_string.flags(); + Some(string_kind.middle_token()) } /// Lex a string literal. @@ -1403,9 +1424,9 @@ impl<'src> Lexer<'src> { // i.e., it recovered from an unclosed parenthesis (`(`, `[`, or `{`). self.nesting -= 1; - // The lexer can't be moved back for a triple-quoted f-string because the newlines are - // part of the f-string itself, so there is no newline token to be emitted. - if self.current_flags.is_triple_quoted_fstring() { + // The lexer can't be moved back for a triple-quoted f/t-string because the newlines are + // part of the f/t-string itself, so there is no newline token to be emitted. + if self.current_flags.is_triple_quoted_interpolated_string() { return false; } @@ -1478,7 +1499,7 @@ impl<'src> Lexer<'src> { nesting: self.nesting, indentations_checkpoint: self.indentations.checkpoint(), pending_indentation: self.pending_indentation, - fstrings_checkpoint: self.fstrings.checkpoint(), + interpolated_strings_checkpoint: self.interpolated_strings.checkpoint(), errors_position: self.errors.len(), } } @@ -1495,7 +1516,7 @@ impl<'src> Lexer<'src> { nesting, indentations_checkpoint, pending_indentation, - fstrings_checkpoint, + interpolated_strings_checkpoint, errors_position, } = checkpoint; @@ -1512,7 +1533,8 @@ impl<'src> Lexer<'src> { self.nesting = nesting; self.indentations.rewind(indentations_checkpoint); self.pending_indentation = pending_indentation; - self.fstrings.rewind(fstrings_checkpoint); + self.interpolated_strings + .rewind(interpolated_strings_checkpoint); self.errors.truncate(errors_position); } @@ -1531,7 +1553,7 @@ pub(crate) struct LexerCheckpoint { nesting: u32, indentations_checkpoint: IndentationsCheckpoint, pending_indentation: Option, - fstrings_checkpoint: FStringsCheckpoint, + interpolated_strings_checkpoint: InterpolatedStringsCheckpoint, errors_position: usize, } @@ -2450,6 +2472,190 @@ f"{(lambda x:{x})}" assert_snapshot!(lex_source(source)); } + #[test] + fn test_empty_tstrings() { + let source = r#"t"" "" t"" t'' '' t"""""" t''''''"#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_prefix() { + let source = r#"t"" t"" rt"" rt"" Rt"" Rt"" tr"" Tr"" tR"" TR"""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring() { + let source = r#"t"normal {foo} {{another}} {bar} {{{three}}}""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_parentheses() { + let source = r#"t"{}" t"{{}}" t" {}" t"{{{}}}" t"{{{{}}}}" t" {} {{}} {{{}}} {{{{}}}} ""#; + assert_snapshot!(lex_source(source)); + } + + fn tstring_single_quote_escape_eol(eol: &str) -> LexerOutput { + let source = format!(r"t'text \{eol} more text'"); + lex_source(&source) + } + + #[test] + fn test_tstring_single_quote_escape_unix_eol() { + assert_snapshot!(tstring_single_quote_escape_eol(UNIX_EOL)); + } + + #[test] + fn test_tstring_single_quote_escape_mac_eol() { + assert_snapshot!(tstring_single_quote_escape_eol(MAC_EOL)); + } + + #[test] + fn test_tstring_single_quote_escape_windows_eol() { + assert_snapshot!(tstring_single_quote_escape_eol(WINDOWS_EOL)); + } + + #[test] + fn test_tstring_escape() { + let source = r#"t"\{x:\"\{x}} \"\"\ + end""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_escape_braces() { + let source = r"t'\{foo}' t'\\{foo}' t'\{{foo}}' t'\\{{foo}}'"; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_escape_raw() { + let source = r#"rt"\{x:\"\{x}} \"\"\ + end""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_named_unicode() { + let source = r#"t"\N{BULLET} normal \Nope \N""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_named_unicode_raw() { + let source = r#"rt"\N{BULLET} normal""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_with_named_expression() { + let source = r#"t"{x:=10} {(x:=10)} {x,{y:=10}} {[x:=10]}""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_with_format_spec() { + let source = r#"t"{foo:} {x=!s:.3f} {x:.{y}f} {'':*^{1:{1}}} {x:{{1}.pop()}}""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_with_multiline_format_spec() { + // The last t-string is invalid syntactically but we should still lex it. + // Note that the `b` is a `Name` token and not a `TStringMiddle` token. + let source = r"t'''__{ + x:d +}__''' +t'''__{ + x:a + b + c +}__''' +t'__{ + x:d +}__' +t'__{ + x:a + b +}__' +"; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_conversion() { + let source = r#"t"{x!s} {x=!r} {x:.3f!r} {{x!r}}""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_nested() { + let source = r#"t"foo {t"bar {x + t"{wow}"}"} baz" t'foo {t'bar'} some {t"another"}'"#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_expression_multiline() { + let source = r#"t"first { + x + * + y +} second""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_multiline() { + let source = r#"t""" +hello + world +""" t''' + world +hello +''' t"some {t"""multiline +allowed {x}"""} string""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_comments() { + let source = r#"t""" +# not a comment { # comment { + x +} # not a comment +""""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_with_ipy_escape_command() { + let source = r#"t"foo {!pwd} bar""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_with_lambda_expression() { + let source = r#" +t"{lambda x:{x}}" +t"{(lambda x:{x})}" +"# + .trim(); + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_with_nul_char() { + let source = r"t'\0'"; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_nested_t_and_fstring() { + let source = r#"t"foo {f"bar {x + t"{wow}"}"} baz" f'foo {t'bar'!r} some {f"another"}'"#; + assert_snapshot!(lex_source(source)); + } + #[test] fn test_match_softkeyword_in_notebook() { let source = r"match foo: @@ -2458,7 +2664,7 @@ f"{(lambda x:{x})}" assert_snapshot!(lex_jupyter_source(source)); } - fn lex_fstring_error(source: &str) -> FStringErrorType { + fn lex_fstring_error(source: &str) -> InterpolatedStringErrorType { let output = lex(source, Mode::Module, TextSize::default()); match output .errors @@ -2474,7 +2680,9 @@ f"{(lambda x:{x})}" #[test] fn test_fstring_error() { - use FStringErrorType::{SingleRbrace, UnterminatedString, UnterminatedTripleQuotedString}; + use InterpolatedStringErrorType::{ + SingleRbrace, UnterminatedString, UnterminatedTripleQuotedString, + }; assert_eq!(lex_fstring_error("f'}'"), SingleRbrace); assert_eq!(lex_fstring_error("f'{{}'"), SingleRbrace); @@ -2499,4 +2707,48 @@ f"{(lambda x:{x})}" UnterminatedTripleQuotedString ); } + + fn lex_tstring_error(source: &str) -> InterpolatedStringErrorType { + let output = lex(source, Mode::Module, TextSize::default()); + match output + .errors + .into_iter() + .next() + .expect("lexer should give at least one error") + .into_error() + { + LexicalErrorType::TStringError(error) => error, + err => panic!("Expected TStringError: {err:?}"), + } + } + + #[test] + fn test_tstring_error() { + use InterpolatedStringErrorType::{ + SingleRbrace, UnterminatedString, UnterminatedTripleQuotedString, + }; + + assert_eq!(lex_tstring_error("t'}'"), SingleRbrace); + assert_eq!(lex_tstring_error("t'{{}'"), SingleRbrace); + assert_eq!(lex_tstring_error("t'{{}}}'"), SingleRbrace); + assert_eq!(lex_tstring_error("t'foo}'"), SingleRbrace); + assert_eq!(lex_tstring_error(r"t'\u007b}'"), SingleRbrace); + assert_eq!(lex_tstring_error("t'{a:b}}'"), SingleRbrace); + assert_eq!(lex_tstring_error("t'{3:}}>10}'"), SingleRbrace); + assert_eq!(lex_tstring_error(r"t'\{foo}\}'"), SingleRbrace); + + assert_eq!(lex_tstring_error(r#"t""#), UnterminatedString); + assert_eq!(lex_tstring_error(r"t'"), UnterminatedString); + + assert_eq!(lex_tstring_error(r#"t""""#), UnterminatedTripleQuotedString); + assert_eq!(lex_tstring_error(r"t'''"), UnterminatedTripleQuotedString); + assert_eq!( + lex_tstring_error(r#"t"""""#), + UnterminatedTripleQuotedString + ); + assert_eq!( + lex_tstring_error(r#"t""""""#), + UnterminatedTripleQuotedString + ); + } } diff --git a/crates/ruff_python_parser/src/lexer/fstring.rs b/crates/ruff_python_parser/src/lexer/interpolated_string.rs similarity index 61% rename from crates/ruff_python_parser/src/lexer/fstring.rs rename to crates/ruff_python_parser/src/lexer/interpolated_string.rs index 7b702a77b7..826edfa796 100644 --- a/crates/ruff_python_parser/src/lexer/fstring.rs +++ b/crates/ruff_python_parser/src/lexer/interpolated_string.rs @@ -1,31 +1,45 @@ use ruff_python_ast::StringFlags; +use crate::string::InterpolatedStringKind; + use super::TokenFlags; -/// The context representing the current f-string that the lexer is in. +/// The context representing the current f-string or t-string that the lexer is in. #[derive(Clone, Debug)] -pub(crate) struct FStringContext { +pub(crate) struct InterpolatedStringContext { flags: TokenFlags, - /// The level of nesting for the lexer when it entered the current f-string. + /// The level of nesting for the lexer when it entered the current f/t-string. /// The nesting level includes all kinds of parentheses i.e., round, square, /// and curly. nesting: u32, - /// The current depth of format spec for the current f-string. This is because + /// The current depth of format spec for the current f/t-string. This is because /// there can be multiple format specs nested for the same f-string. /// For example, `{a:{b:{c}}}` has 3 format specs. format_spec_depth: u32, } -impl FStringContext { - pub(crate) const fn new(flags: TokenFlags, nesting: u32) -> Self { - assert!(flags.is_f_string()); +impl InterpolatedStringContext { + pub(crate) const fn new(flags: TokenFlags, nesting: u32) -> Option { + if flags.is_interpolated_string() { + Some(Self { + flags, + nesting, + format_spec_depth: 0, + }) + } else { + None + } + } - Self { - flags, - nesting, - format_spec_depth: 0, + pub(crate) fn kind(&self) -> InterpolatedStringKind { + if self.flags.is_f_string() { + InterpolatedStringKind::FString + } else if self.flags.is_t_string() { + InterpolatedStringKind::TString + } else { + unreachable!("Can only be constructed when f-string or t-string flag is present") } } @@ -68,15 +82,15 @@ impl FStringContext { current_nesting.saturating_sub(self.nesting) } - /// Returns `true` if the lexer is in a f-string expression i.e., between + /// Returns `true` if the lexer is in an f-string expression or t-string interpolation i.e., between /// two curly braces. - pub(crate) const fn is_in_expression(&self, current_nesting: u32) -> bool { + pub(crate) const fn is_in_interpolation(&self, current_nesting: u32) -> bool { self.open_parentheses_count(current_nesting) > self.format_spec_depth } /// Returns `true` if the lexer is in a f-string format spec i.e., after a colon. pub(crate) const fn is_in_format_spec(&self, current_nesting: u32) -> bool { - self.format_spec_depth > 0 && !self.is_in_expression(current_nesting) + self.format_spec_depth > 0 && !self.is_in_interpolation(current_nesting) } /// Returns `true` if the context is in a valid position to start format spec @@ -106,38 +120,38 @@ impl FStringContext { } } -/// The f-strings stack is used to keep track of all the f-strings that the -/// lexer encounters. This is necessary because f-strings can be nested. +/// The interpolated strings stack is used to keep track of all the f-strings and t-strings that the +/// lexer encounters. This is necessary because f-strings and t-strings can be nested. #[derive(Debug, Default)] -pub(crate) struct FStrings { - stack: Vec, +pub(crate) struct InterpolatedStrings { + stack: Vec, } -impl FStrings { - pub(crate) fn push(&mut self, context: FStringContext) { +impl InterpolatedStrings { + pub(crate) fn push(&mut self, context: InterpolatedStringContext) { self.stack.push(context); } - pub(crate) fn pop(&mut self) -> Option { + pub(crate) fn pop(&mut self) -> Option { self.stack.pop() } - pub(crate) fn current(&self) -> Option<&FStringContext> { + pub(crate) fn current(&self) -> Option<&InterpolatedStringContext> { self.stack.last() } - pub(crate) fn current_mut(&mut self) -> Option<&mut FStringContext> { + pub(crate) fn current_mut(&mut self) -> Option<&mut InterpolatedStringContext> { self.stack.last_mut() } - pub(crate) fn checkpoint(&self) -> FStringsCheckpoint { - FStringsCheckpoint(self.stack.clone()) + pub(crate) fn checkpoint(&self) -> InterpolatedStringsCheckpoint { + InterpolatedStringsCheckpoint(self.stack.clone()) } - pub(crate) fn rewind(&mut self, checkpoint: FStringsCheckpoint) { + pub(crate) fn rewind(&mut self, checkpoint: InterpolatedStringsCheckpoint) { self.stack = checkpoint.0; } } #[derive(Debug, Clone)] -pub(crate) struct FStringsCheckpoint(Vec); +pub(crate) struct InterpolatedStringsCheckpoint(Vec); diff --git a/crates/ruff_python_parser/src/lib.rs b/crates/ruff_python_parser/src/lib.rs index fa5230a016..346bd89aa8 100644 --- a/crates/ruff_python_parser/src/lib.rs +++ b/crates/ruff_python_parser/src/lib.rs @@ -67,8 +67,8 @@ use std::iter::FusedIterator; use std::ops::Deref; pub use crate::error::{ - FStringErrorType, LexicalErrorType, ParseError, ParseErrorType, UnsupportedSyntaxError, - UnsupportedSyntaxErrorKind, + InterpolatedStringErrorType, LexicalErrorType, ParseError, ParseErrorType, + UnsupportedSyntaxError, UnsupportedSyntaxErrorKind, }; pub use crate::parser::ParseOptions; pub use crate::token::{Token, TokenKind}; diff --git a/crates/ruff_python_parser/src/parser/expression.rs b/crates/ruff_python_parser/src/parser/expression.rs index 3a3d91ce75..73ce206664 100644 --- a/crates/ruff_python_parser/src/parser/expression.rs +++ b/crates/ruff_python_parser/src/parser/expression.rs @@ -6,22 +6,27 @@ use rustc_hash::{FxBuildHasher, FxHashSet}; use ruff_python_ast::name::Name; use ruff_python_ast::{ - self as ast, BoolOp, CmpOp, ConversionFlag, Expr, ExprContext, FStringElement, FStringElements, - IpyEscapeKind, Number, Operator, OperatorPrecedence, StringFlags, UnaryOp, + self as ast, AnyStringFlags, BoolOp, CmpOp, ConversionFlag, Expr, ExprContext, FString, + InterpolatedStringElement, InterpolatedStringElements, IpyEscapeKind, Number, Operator, + OperatorPrecedence, StringFlags, TString, UnaryOp, }; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use crate::error::{FStringKind, StarTupleKind, UnparenthesizedNamedExprKind}; use crate::parser::progress::ParserProgress; use crate::parser::{FunctionKind, Parser, helpers}; -use crate::string::{StringType, parse_fstring_literal_element, parse_string_literal}; +use crate::string::{ + InterpolatedStringKind, StringType, parse_interpolated_string_literal_element, + parse_string_literal, +}; use crate::token::{TokenKind, TokenValue}; use crate::token_set::TokenSet; use crate::{ - FStringErrorType, Mode, ParseErrorType, UnsupportedSyntaxError, UnsupportedSyntaxErrorKind, + InterpolatedStringErrorType, Mode, ParseErrorType, UnsupportedSyntaxError, + UnsupportedSyntaxErrorKind, }; -use super::{FStringElementsKind, Parenthesized, RecoveryContextKind}; +use super::{InterpolatedStringElementsKind, Parenthesized, RecoveryContextKind}; /// A token set consisting of a newline or end of file. const NEWLINE_EOF_SET: TokenSet = TokenSet::new([TokenKind::Newline, TokenKind::EndOfFile]); @@ -54,6 +59,7 @@ pub(super) const EXPR_SET: TokenSet = TokenSet::new([ TokenKind::Not, TokenKind::Yield, TokenKind::FStringStart, + TokenKind::TStringStart, TokenKind::IpyEscapeCommand, ]) .union(LITERAL_SET); @@ -581,7 +587,9 @@ impl<'src> Parser<'src> { TokenKind::IpyEscapeCommand => { Expr::IpyEscapeCommand(self.parse_ipython_escape_command_expression()) } - TokenKind::String | TokenKind::FStringStart => self.parse_strings(), + TokenKind::String | TokenKind::FStringStart | TokenKind::TStringStart => { + self.parse_strings() + } TokenKind::Lpar => { return self.parse_parenthesized_expression(); } @@ -1177,12 +1185,15 @@ impl<'src> Parser<'src> { /// /// # Panics /// - /// If the parser isn't positioned at a `String` or `FStringStart` token. + /// If the parser isn't positioned at a `String`, `FStringStart`, or `TStringStart` token. /// /// See: (Search "strings:") pub(super) fn parse_strings(&mut self) -> Expr { - const STRING_START_SET: TokenSet = - TokenSet::new([TokenKind::String, TokenKind::FStringStart]); + const STRING_START_SET: TokenSet = TokenSet::new([ + TokenKind::String, + TokenKind::FStringStart, + TokenKind::TStringStart, + ]); let start = self.node_start(); let mut strings = vec![]; @@ -1194,8 +1205,16 @@ impl<'src> Parser<'src> { if self.at(TokenKind::String) { strings.push(self.parse_string_or_byte_literal()); - } else { - strings.push(StringType::FString(self.parse_fstring())); + } else if self.at(TokenKind::FStringStart) { + strings.push(StringType::FString( + self.parse_interpolated_string(InterpolatedStringKind::FString) + .into(), + )); + } else if self.at(TokenKind::TStringStart) { + strings.push(StringType::TString( + self.parse_interpolated_string(InterpolatedStringKind::TString) + .into(), + )); } } @@ -1219,6 +1238,10 @@ impl<'src> Parser<'src> { value: ast::FStringValue::single(fstring), range, }), + StringType::TString(tstring) => Expr::TString(ast::ExprTString { + value: ast::TStringValue::single(tstring), + range, + }), }, _ => self.handle_implicitly_concatenated_strings(strings, range), } @@ -1236,11 +1259,13 @@ impl<'src> Parser<'src> { ) -> Expr { assert!(strings.len() > 1); + let mut has_tstring = false; let mut has_fstring = false; let mut byte_literal_count = 0; for string in &strings { match string { StringType::FString(_) => has_fstring = true, + StringType::TString(_) => has_tstring = true, StringType::Bytes(_) => byte_literal_count += 1, StringType::Str(_) => {} } @@ -1269,7 +1294,7 @@ impl<'src> Parser<'src> { ); } // Only construct a byte expression if all the literals are bytes - // otherwise, we'll try either string or f-string. This is to retain + // otherwise, we'll try either string, t-string, or f-string. This is to retain // as much information as possible. Ordering::Equal => { let mut values = Vec::with_capacity(strings.len()); @@ -1310,7 +1335,7 @@ impl<'src> Parser<'src> { // ) // 2 + 2 - if !has_fstring { + if !has_fstring && !has_tstring { let mut values = Vec::with_capacity(strings.len()); for string in strings { values.push(match string { @@ -1324,10 +1349,34 @@ impl<'src> Parser<'src> { }); } + if has_tstring { + let mut parts = Vec::with_capacity(strings.len()); + for string in strings { + match string { + StringType::TString(tstring) => parts.push(ast::TStringPart::TString(tstring)), + StringType::FString(fstring) => { + parts.push(ruff_python_ast::TStringPart::FString(fstring)); + } + StringType::Str(string) => parts.push(ast::TStringPart::Literal(string)), + StringType::Bytes(bytes) => parts.push(ast::TStringPart::Literal( + ast::StringLiteral::invalid(bytes.range()), + )), + } + } + + return Expr::from(ast::ExprTString { + value: ast::TStringValue::concatenated(parts), + range, + }); + } + let mut parts = Vec::with_capacity(strings.len()); for string in strings { match string { StringType::FString(fstring) => parts.push(ast::FStringPart::FString(fstring)), + StringType::TString(_) => { + unreachable!("expected no tstring parts by this point") + } StringType::Str(string) => parts.push(ast::FStringPart::Literal(string)), StringType::Bytes(bytes) => parts.push(ast::FStringPart::Literal( ast::StringLiteral::invalid(bytes.range()), @@ -1388,24 +1437,32 @@ impl<'src> Parser<'src> { } } - /// Parses a f-string. + /// Parses an f/t-string. /// /// This does not handle implicitly concatenated strings. /// /// # Panics /// - /// If the parser isn't positioned at a `FStringStart` token. + /// If the parser isn't positioned at an `FStringStart` or + /// `TStringStart` token. /// - /// See: (Search "fstring:") + /// See: (Search "fstring:" or "tstring:") /// See: - fn parse_fstring(&mut self) -> ast::FString { + fn parse_interpolated_string( + &mut self, + kind: InterpolatedStringKind, + ) -> InterpolatedStringData { let start = self.node_start(); let flags = self.tokens.current_flags().as_any_string_flags(); - self.bump(TokenKind::FStringStart); - let elements = self.parse_fstring_elements(flags, FStringElementsKind::Regular); + self.bump(kind.start_token()); + let elements = self.parse_interpolated_string_elements( + flags, + InterpolatedStringElementsKind::Regular, + kind, + ); - self.expect(TokenKind::FStringEnd); + self.expect(kind.end_token()); // test_ok pep701_f_string_py312 // # parse_options: {"target-version": "3.12"} @@ -1419,6 +1476,18 @@ impl<'src> Parser<'src> { // f"test {a \ // } more" # line continuation + // test_ok pep750_t_string_py314 + // # 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 + // test_ok pep701_f_string_py311 // # parse_options: {"target-version": "3.11"} // f"outer {'# not a comment'}" @@ -1444,10 +1513,12 @@ impl<'src> Parser<'src> { let range = self.node_range(start); - if !self.options.target_version.supports_pep_701() { + if !self.options.target_version.supports_pep_701() + && matches!(kind, InterpolatedStringKind::FString) + { let quote_bytes = flags.quote_str().as_bytes(); let quote_len = flags.quote_len(); - for expr in elements.expressions() { + for expr in elements.interpolations() { for slash_position in memchr::memchr_iter(b'\\', self.source[expr.range].as_bytes()) { let slash_position = TextSize::try_from(slash_position).unwrap(); @@ -1471,10 +1542,10 @@ impl<'src> Parser<'src> { self.check_fstring_comments(range); } - ast::FString { + InterpolatedStringData { elements, range, - flags: ast::FStringFlags::from(flags), + flags, } } @@ -1490,80 +1561,87 @@ impl<'src> Parser<'src> { })); } - /// Parses a list of f-string elements. + /// Parses a list of f/t-string elements. /// /// # Panics /// - /// If the parser isn't positioned at a `{` or `FStringMiddle` token. - fn parse_fstring_elements( + /// If the parser isn't positioned at a `{`, `FStringMiddle`, + /// or `TStringMiddle` token. + fn parse_interpolated_string_elements( &mut self, flags: ast::AnyStringFlags, - kind: FStringElementsKind, - ) -> FStringElements { + elements_kind: InterpolatedStringElementsKind, + string_kind: InterpolatedStringKind, + ) -> ast::InterpolatedStringElements { let mut elements = vec![]; + let middle_token_kind = string_kind.middle_token(); - self.parse_list(RecoveryContextKind::FStringElements(kind), |parser| { - let element = match parser.current_token_kind() { - TokenKind::Lbrace => { - FStringElement::Expression(parser.parse_fstring_expression_element(flags)) - } - TokenKind::FStringMiddle => { - let range = parser.current_token_range(); - let TokenValue::FStringMiddle(value) = - parser.bump_value(TokenKind::FStringMiddle) - else { - unreachable!() - }; - FStringElement::Literal( - parse_fstring_literal_element(value, flags, range).unwrap_or_else( - |lex_error| { - // test_err invalid_fstring_literal_element - // f'hello \N{INVALID} world' - // f"""hello \N{INVALID} world""" - let location = lex_error.location(); - parser.add_error( - ParseErrorType::Lexical(lex_error.into_error()), - location, - ); - ast::FStringLiteralElement { - value: "".into(), - range, - } - }, - ), - ) - } - // `Invalid` tokens are created when there's a lexical error, so - // we ignore it here to avoid creating unexpected token errors - TokenKind::Unknown => { - parser.bump_any(); - return; - } - tok => { - // This should never happen because the list parsing will only - // call this closure for the above token kinds which are the same - // as in the FIRST set. - unreachable!( - "f-string: unexpected token `{tok:?}` at {:?}", - parser.current_token_range() - ); - } - }; - elements.push(element); - }); + self.parse_list( + RecoveryContextKind::InterpolatedStringElements(elements_kind), + |parser| { + let element = match parser.current_token_kind() { + TokenKind::Lbrace => ast::InterpolatedStringElement::from( + parser.parse_interpolated_element(flags, string_kind), + ), + tok if tok == middle_token_kind => { + let range = parser.current_token_range(); + let TokenValue::InterpolatedStringMiddle(value) = + parser.bump_value(middle_token_kind) + else { + unreachable!() + }; + InterpolatedStringElement::Literal( + parse_interpolated_string_literal_element(value, flags, range) + .unwrap_or_else(|lex_error| { + // test_err invalid_fstring_literal_element + // f'hello \N{INVALID} world' + // f"""hello \N{INVALID} world""" + let location = lex_error.location(); + parser.add_error( + ParseErrorType::Lexical(lex_error.into_error()), + location, + ); + ast::InterpolatedStringLiteralElement { + value: "".into(), + range, + } + }), + ) + } + // `Invalid` tokens are created when there's a lexical error, so + // we ignore it here to avoid creating unexpected token errors + TokenKind::Unknown => { + parser.bump_any(); + return; + } + tok => { + // This should never happen because the list parsing will only + // call this closure for the above token kinds which are the same + // as in the FIRST set. + unreachable!( + "{}: unexpected token `{tok:?}` at {:?}", + string_kind, + parser.current_token_range() + ); + } + }; + elements.push(element); + }, + ); - FStringElements::from(elements) + ast::InterpolatedStringElements::from(elements) } - /// Parses a f-string expression element. + /// Parses an f/t-string expression element. /// /// # Panics /// /// If the parser isn't positioned at a `{` token. - fn parse_fstring_expression_element( + fn parse_interpolated_element( &mut self, flags: ast::AnyStringFlags, - ) -> ast::FStringExpressionElement { + string_kind: InterpolatedStringKind, + ) -> ast::InterpolatedElement { let start = self.node_start(); self.bump(TokenKind::Lbrace); @@ -1571,11 +1649,23 @@ impl<'src> Parser<'src> { // f"{}" // f"{ }" + // test_err t_string_empty_expression + // # parse_options: {"target-version": "3.14"} + // t"{}" + // t"{ }" + // test_err f_string_invalid_starred_expr // # Starred expression inside f-string has a minimum precedence of bitwise or. // f"{*}" // f"{*x and y}" // f"{*yield x}" + + // test_err t_string_invalid_starred_expr + // # 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}" let value = self.parse_expression_list(ExpressionContext::yield_or_starred_bitwise_or()); if !value.is_parenthesized && value.expr.is_lambda_expr() { @@ -1585,8 +1675,15 @@ impl<'src> Parser<'src> { // test_err f_string_lambda_without_parentheses // f"{lambda x: x}" + + // test_err t_string_lambda_without_parentheses + // # parse_options: {"target-version": "3.14"} + // t"{lambda x: x}" self.add_error( - ParseErrorType::FStringError(FStringErrorType::LambdaWithoutParentheses), + ParseErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::LambdaWithoutParentheses, + string_kind, + ), value.range(), ); } @@ -1614,8 +1711,15 @@ impl<'src> Parser<'src> { _ => { // test_err f_string_invalid_conversion_flag_name_tok // f"{x!z}" + + // test_err t_string_invalid_conversion_flag_name_tok + // # parse_options: {"target-version": "3.14"} + // t"{x!z}" self.add_error( - ParseErrorType::FStringError(FStringErrorType::InvalidConversionFlag), + ParseErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::InvalidConversionFlag, + string_kind, + ), conversion_flag_range, ); ConversionFlag::None @@ -1625,8 +1729,16 @@ impl<'src> Parser<'src> { // test_err f_string_invalid_conversion_flag_other_tok // f"{x!123}" // f"{x!'a'}" + + // test_err t_string_invalid_conversion_flag_other_tok + // # parse_options: {"target-version": "3.14"} + // t"{x!123}" + // t"{x!'a'}" self.add_error( - ParseErrorType::FStringError(FStringErrorType::InvalidConversionFlag), + ParseErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::InvalidConversionFlag, + string_kind, + ), conversion_flag_range, ); // TODO(dhruvmanila): Avoid dropping this token @@ -1639,8 +1751,12 @@ impl<'src> Parser<'src> { let format_spec = if self.eat(TokenKind::Colon) { let spec_start = self.node_start(); - let elements = self.parse_fstring_elements(flags, FStringElementsKind::FormatSpec); - Some(Box::new(ast::FStringFormatSpec { + let elements = self.parse_interpolated_string_elements( + flags, + InterpolatedStringElementsKind::FormatSpec, + string_kind, + ); + Some(Box::new(ast::InterpolatedStringFormatSpec { range: self.node_range(spec_start), elements, })) @@ -1661,18 +1777,34 @@ impl<'src> Parser<'src> { // f"{" // f"""{""" + // test_err t_string_unclosed_lbrace + // # parse_options: {"target-version": "3.14"} + // t"{" + // t"{foo!r" + // t"{foo=" + // t"{" + // t"""{""" + // The lexer does emit `FStringEnd` for the following test cases: // test_err f_string_unclosed_lbrace_in_format_spec // f"hello {x:" // f"hello {x:.3f" + + // test_err t_string_unclosed_lbrace_in_format_spec + // # parse_options: {"target-version": "3.14"} + // t"hello {x:" + // t"hello {x:.3f" self.add_error( - ParseErrorType::FStringError(FStringErrorType::UnclosedLbrace), + ParseErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::UnclosedLbrace, + string_kind, + ), self.current_token_range(), ); } - ast::FStringExpressionElement { + ast::InterpolatedElement { expression: Box::new(value.expr), debug_text, conversion, @@ -2755,3 +2887,30 @@ impl ExpressionContext { } } } + +#[derive(Debug)] +struct InterpolatedStringData { + elements: InterpolatedStringElements, + range: TextRange, + flags: AnyStringFlags, +} + +impl From for FString { + fn from(value: InterpolatedStringData) -> Self { + Self { + elements: value.elements, + range: value.range, + flags: value.flags.into(), + } + } +} + +impl From for TString { + fn from(value: InterpolatedStringData) -> Self { + Self { + elements: value.elements, + range: value.range, + flags: value.flags.into(), + } + } +} diff --git a/crates/ruff_python_parser/src/parser/helpers.rs b/crates/ruff_python_parser/src/parser/helpers.rs index e7a0e426c2..de89746333 100644 --- a/crates/ruff_python_parser/src/parser/helpers.rs +++ b/crates/ruff_python_parser/src/parser/helpers.rs @@ -94,6 +94,7 @@ pub(super) fn detect_invalid_pre_py39_decorator_node( Expr::YieldFrom(_) => "`yield from` expression", Expr::Compare(_) => "comparison expression", Expr::FString(_) => "f-string", + Expr::TString(_) => "t-string", Expr::Named(_) => "assignment expression", Expr::Subscript(_) => "subscript expression", Expr::IpyEscapeCommand(_) => "IPython escape command", diff --git a/crates/ruff_python_parser/src/parser/mod.rs b/crates/ruff_python_parser/src/parser/mod.rs index 19596364a8..0668f18b29 100644 --- a/crates/ruff_python_parser/src/parser/mod.rs +++ b/crates/ruff_python_parser/src/parser/mod.rs @@ -798,7 +798,7 @@ impl WithItemKind { } #[derive(Debug, PartialEq, Copy, Clone)] -enum FStringElementsKind { +enum InterpolatedStringElementsKind { /// The regular f-string elements. /// /// For example, the `"hello "`, `x`, and `" world"` elements in: @@ -816,14 +816,16 @@ enum FStringElementsKind { FormatSpec, } -impl FStringElementsKind { - const fn list_terminator(self) -> TokenKind { +impl InterpolatedStringElementsKind { + const fn list_terminators(self) -> TokenSet { match self { - FStringElementsKind::Regular => TokenKind::FStringEnd, + InterpolatedStringElementsKind::Regular => { + TokenSet::new([TokenKind::FStringEnd, TokenKind::TStringEnd]) + } // test_ok fstring_format_spec_terminator // f"hello {x:} world" // f"hello {x:.3f} world" - FStringElementsKind::FormatSpec => TokenKind::Rbrace, + InterpolatedStringElementsKind::FormatSpec => TokenSet::new([TokenKind::Rbrace]), } } } @@ -931,9 +933,8 @@ enum RecoveryContextKind { /// When parsing a list of items in a `with` statement WithItems(WithItemKind), - /// When parsing a list of f-string elements which are either literal elements - /// or expressions. - FStringElements(FStringElementsKind), + /// When parsing a list of f-string or t-string elements which are either literal elements, expressions, or interpolations. + InterpolatedStringElements(InterpolatedStringElementsKind), } impl RecoveryContextKind { @@ -1117,8 +1118,8 @@ impl RecoveryContextKind { .at(TokenKind::Colon) .then_some(ListTerminatorKind::Regular), }, - RecoveryContextKind::FStringElements(kind) => { - if p.at(kind.list_terminator()) { + RecoveryContextKind::InterpolatedStringElements(kind) => { + if p.at_ts(kind.list_terminators()) { Some(ListTerminatorKind::Regular) } else { // test_err unterminated_fstring_newline_recovery @@ -1174,10 +1175,10 @@ impl RecoveryContextKind { ) || p.at_name_or_soft_keyword() } RecoveryContextKind::WithItems(_) => p.at_expr(), - RecoveryContextKind::FStringElements(_) => matches!( + RecoveryContextKind::InterpolatedStringElements(_) => matches!( p.current_token_kind(), // Literal element - TokenKind::FStringMiddle + TokenKind::FStringMiddle | TokenKind::TStringMiddle // Expression element | TokenKind::Lbrace ), @@ -1268,13 +1269,13 @@ impl RecoveryContextKind { "Expected an expression or the end of the with item list".to_string(), ), }, - RecoveryContextKind::FStringElements(kind) => match kind { - FStringElementsKind::Regular => ParseErrorType::OtherError( - "Expected an f-string element or the end of the f-string".to_string(), + RecoveryContextKind::InterpolatedStringElements(kind) => match kind { + InterpolatedStringElementsKind::Regular => ParseErrorType::OtherError( + "Expected an f-string or t-string element or the end of the f-string or t-string".to_string(), + ), + InterpolatedStringElementsKind::FormatSpec => ParseErrorType::OtherError( + "Expected an f-string or t-string element or a '}'".to_string(), ), - FStringElementsKind::FormatSpec => { - ParseErrorType::OtherError("Expected an f-string element or a '}'".to_string()) - } }, } } @@ -1313,8 +1314,8 @@ bitflags! { const WITH_ITEMS_PARENTHESIZED = 1 << 25; const WITH_ITEMS_PARENTHESIZED_EXPRESSION = 1 << 26; const WITH_ITEMS_UNPARENTHESIZED = 1 << 28; - const F_STRING_ELEMENTS = 1 << 29; - const F_STRING_ELEMENTS_IN_FORMAT_SPEC = 1 << 30; + const FT_STRING_ELEMENTS = 1 << 29; + const FT_STRING_ELEMENTS_IN_FORMAT_SPEC = 1 << 30; } } @@ -1367,10 +1368,10 @@ impl RecoveryContext { } WithItemKind::Unparenthesized => RecoveryContext::WITH_ITEMS_UNPARENTHESIZED, }, - RecoveryContextKind::FStringElements(kind) => match kind { - FStringElementsKind::Regular => RecoveryContext::F_STRING_ELEMENTS, - FStringElementsKind::FormatSpec => { - RecoveryContext::F_STRING_ELEMENTS_IN_FORMAT_SPEC + RecoveryContextKind::InterpolatedStringElements(kind) => match kind { + InterpolatedStringElementsKind::Regular => RecoveryContext::FT_STRING_ELEMENTS, + InterpolatedStringElementsKind::FormatSpec => { + RecoveryContext::FT_STRING_ELEMENTS_IN_FORMAT_SPEC } }, } @@ -1439,11 +1440,13 @@ impl RecoveryContext { RecoveryContext::WITH_ITEMS_UNPARENTHESIZED => { RecoveryContextKind::WithItems(WithItemKind::Unparenthesized) } - RecoveryContext::F_STRING_ELEMENTS => { - RecoveryContextKind::FStringElements(FStringElementsKind::Regular) - } - RecoveryContext::F_STRING_ELEMENTS_IN_FORMAT_SPEC => { - RecoveryContextKind::FStringElements(FStringElementsKind::FormatSpec) + RecoveryContext::FT_STRING_ELEMENTS => RecoveryContextKind::InterpolatedStringElements( + InterpolatedStringElementsKind::Regular, + ), + RecoveryContext::FT_STRING_ELEMENTS_IN_FORMAT_SPEC => { + RecoveryContextKind::InterpolatedStringElements( + InterpolatedStringElementsKind::FormatSpec, + ) } _ => return None, }) diff --git a/crates/ruff_python_parser/src/parser/pattern.rs b/crates/ruff_python_parser/src/parser/pattern.rs index ced1627461..461b859c78 100644 --- a/crates/ruff_python_parser/src/parser/pattern.rs +++ b/crates/ruff_python_parser/src/parser/pattern.rs @@ -390,7 +390,7 @@ impl Parser<'_> { range: self.node_range(start), }) } - TokenKind::String | TokenKind::FStringStart => { + TokenKind::String | TokenKind::FStringStart | TokenKind::TStringStart => { let str = self.parse_strings(); Pattern::MatchValue(ast::PatternMatchValue { diff --git a/crates/ruff_python_parser/src/parser/statement.rs b/crates/ruff_python_parser/src/parser/statement.rs index 1eb59b25b6..6a44192746 100644 --- a/crates/ruff_python_parser/src/parser/statement.rs +++ b/crates/ruff_python_parser/src/parser/statement.rs @@ -3012,7 +3012,6 @@ impl<'src> Parser<'src> { // test_ok param_with_annotation // def foo(arg: int): ... // def foo(arg: lambda x: x): ... - // def foo(arg: (x := int)): ... // test_err param_with_invalid_annotation // def foo(arg: *int): ... @@ -3703,6 +3702,7 @@ impl<'src> Parser<'src> { | TokenKind::Complex | TokenKind::String | TokenKind::FStringStart + | TokenKind::TStringStart | TokenKind::Lbrace | TokenKind::Tilde | TokenKind::Ellipsis diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__empty_tstrings.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__empty_tstrings.snap new file mode 100644 index 0000000000..d23fee4ae8 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__empty_tstrings.snap @@ -0,0 +1,98 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 2..3, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + String( + "", + ), + 4..6, + TokenFlags( + DOUBLE_QUOTES, + ), + ), + ( + TStringStart, + 7..9, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 9..10, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 11..13, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 13..14, + TokenFlags( + T_STRING, + ), + ), + ( + String( + "", + ), + 15..17, + ), + ( + TStringStart, + 18..22, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringEnd, + 22..25, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringStart, + 26..30, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringEnd, + 30..33, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Newline, + 33..33, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring.snap index 3a56937bcc..c515b59ec0 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "normal ", ), 2..9, @@ -37,7 +36,7 @@ snapshot_kind: text 13..14, ), ( - FStringMiddle( + InterpolatedStringMiddle( " {another} ", ), 14..27, @@ -60,7 +59,7 @@ snapshot_kind: text 31..32, ), ( - FStringMiddle( + InterpolatedStringMiddle( " {", ), 32..35, @@ -83,7 +82,7 @@ snapshot_kind: text 41..42, ), ( - FStringMiddle( + InterpolatedStringMiddle( "}", ), 42..44, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_comments.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_comments.snap index dae04a5f0c..93e0b88bd9 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_comments.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_comments.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\n# not a comment ", ), 4..21, @@ -49,7 +48,7 @@ snapshot_kind: text 41..42, ), ( - FStringMiddle( + InterpolatedStringMiddle( " # not a comment\n", ), 42..59, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_conversion.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_conversion.snap index 80a8683277..cff7b14e12 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_conversion.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_conversion.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -38,7 +37,7 @@ snapshot_kind: text 6..7, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 7..8, @@ -75,7 +74,7 @@ snapshot_kind: text 13..14, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 14..15, @@ -98,7 +97,7 @@ snapshot_kind: text 17..18, ), ( - FStringMiddle( + InterpolatedStringMiddle( ".3f!r", ), 18..23, @@ -111,7 +110,7 @@ snapshot_kind: text 23..24, ), ( - FStringMiddle( + InterpolatedStringMiddle( " {x!r}", ), 24..32, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape.snap index 7aae96b72f..899139162d 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\", ), 2..3, @@ -37,7 +36,7 @@ snapshot_kind: text 5..6, ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\\"\\", ), 6..9, @@ -64,7 +63,7 @@ snapshot_kind: text 12..13, ), ( - FStringMiddle( + InterpolatedStringMiddle( " \\\"\\\"\\\n end", ), 13..24, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_braces.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_braces.snap index 3cfba863a2..a792cfee11 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_braces.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_braces.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\", ), 2..3, @@ -51,7 +50,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\\\", ), 12..14, @@ -88,7 +87,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\{foo}", ), 23..31, @@ -111,7 +110,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\\\{foo}", ), 35..44, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_raw.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_raw.snap index 0e14fbb35d..5fe4b16837 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_raw.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_raw.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\", ), 3..4, @@ -37,7 +36,7 @@ snapshot_kind: text 6..7, ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\\"\\", ), 7..10, @@ -64,7 +63,7 @@ snapshot_kind: text 13..14, ), ( - FStringMiddle( + InterpolatedStringMiddle( " \\\"\\\"\\\n end", ), 14..25, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_expression_multiline.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_expression_multiline.snap index c7fd18b79a..5987a41f67 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_expression_multiline.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_expression_multiline.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "first ", ), 2..8, @@ -63,7 +62,7 @@ snapshot_kind: text 40..41, ), ( - FStringMiddle( + InterpolatedStringMiddle( " second", ), 41..48, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_multiline.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_multiline.snap index 95c43f76d1..15a765a45a 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_multiline.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_multiline.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\nhello\n world\n", ), 4..21, @@ -37,7 +36,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\n world\nhello\n", ), 29..46, @@ -60,7 +59,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "some ", ), 52..57, @@ -80,7 +79,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "multiline\nallowed ", ), 62..80, @@ -114,7 +113,7 @@ snapshot_kind: text 86..87, ), ( - FStringMiddle( + InterpolatedStringMiddle( " string", ), 87..94, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode.snap index 2ae4109360..5571d867a1 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\N{BULLET} normal \\Nope \\N", ), 2..28, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode_raw.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode_raw.snap index b37611f0da..974d2cf9c3 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode_raw.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode_raw.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\N", ), 3..5, @@ -37,7 +36,7 @@ snapshot_kind: text 12..13, ), ( - FStringMiddle( + InterpolatedStringMiddle( " normal", ), 13..20, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_nested.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_nested.snap index de3e6d60f2..8e1dc7e8d2 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_nested.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_nested.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "foo ", ), 2..6, @@ -34,7 +33,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "bar ", ), 9..13, @@ -100,7 +99,7 @@ snapshot_kind: text 28..29, ), ( - FStringMiddle( + InterpolatedStringMiddle( " baz", ), 29..33, @@ -123,7 +122,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "foo ", ), 37..41, @@ -143,7 +142,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "bar", ), 44..47, @@ -163,7 +162,7 @@ snapshot_kind: text 48..49, ), ( - FStringMiddle( + InterpolatedStringMiddle( " some ", ), 49..55, @@ -183,7 +182,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "another", ), 58..65, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_parentheses.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_parentheses.snap index 287d62d08a..381aa8e626 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_parentheses.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_parentheses.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -36,7 +35,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "{}", ), 8..12, @@ -59,7 +58,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 16..17, @@ -90,7 +89,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "{", ), 23..25, @@ -107,7 +106,7 @@ snapshot_kind: text 26..27, ), ( - FStringMiddle( + InterpolatedStringMiddle( "}", ), 27..29, @@ -130,7 +129,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "{{}}", ), 33..41, @@ -153,7 +152,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 45..46, @@ -170,7 +169,7 @@ snapshot_kind: text 47..48, ), ( - FStringMiddle( + InterpolatedStringMiddle( " {} {", ), 48..56, @@ -187,7 +186,7 @@ snapshot_kind: text 57..58, ), ( - FStringMiddle( + InterpolatedStringMiddle( "} {{}} ", ), 58..71, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_mac_eol.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_mac_eol.snap index 5476c1fa02..53a3fe7908 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_mac_eol.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_mac_eol.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: fstring_single_quote_escape_eol(MAC_EOL) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "text \\\r more text", ), 2..19, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_unix_eol.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_unix_eol.snap index 19e0346f43..d8e27f0661 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_unix_eol.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_unix_eol.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: fstring_single_quote_escape_eol(UNIX_EOL) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "text \\\n more text", ), 2..19, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_windows_eol.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_windows_eol.snap index c4f595a389..ba73b4a09d 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_windows_eol.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_windows_eol.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: fstring_single_quote_escape_eol(WINDOWS_EOL) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "text \\\r\n more text", ), 2..20, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_format_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_format_spec.snap index 400f81636f..26380715dc 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_format_spec.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_format_spec.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -32,7 +31,7 @@ snapshot_kind: text 7..8, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 8..9, @@ -69,7 +68,7 @@ snapshot_kind: text 14..15, ), ( - FStringMiddle( + InterpolatedStringMiddle( ".3f", ), 15..18, @@ -82,7 +81,7 @@ snapshot_kind: text 18..19, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 19..20, @@ -105,7 +104,7 @@ snapshot_kind: text 22..23, ), ( - FStringMiddle( + InterpolatedStringMiddle( ".", ), 23..24, @@ -128,7 +127,7 @@ snapshot_kind: text 26..27, ), ( - FStringMiddle( + InterpolatedStringMiddle( "f", ), 27..28, @@ -141,7 +140,7 @@ snapshot_kind: text 28..29, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 29..30, @@ -164,7 +163,7 @@ snapshot_kind: text 33..34, ), ( - FStringMiddle( + InterpolatedStringMiddle( "*^", ), 34..36, @@ -209,7 +208,7 @@ snapshot_kind: text 43..44, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 44..45, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_ipy_escape_command.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_ipy_escape_command.snap index f48c742e6d..523b5d6133 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_ipy_escape_command.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_ipy_escape_command.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "foo ", ), 2..6, @@ -41,7 +40,7 @@ snapshot_kind: text 11..12, ), ( - FStringMiddle( + InterpolatedStringMiddle( " bar", ), 12..16, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_multiline_format_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_multiline_format_spec.snap index 341455e1f2..6a0909bcdb 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_multiline_format_spec.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_multiline_format_spec.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "__", ), 4..6, @@ -41,7 +40,7 @@ snapshot_kind: text 13..14, ), ( - FStringMiddle( + InterpolatedStringMiddle( "d\n", ), 14..16, @@ -54,7 +53,7 @@ snapshot_kind: text 16..17, ), ( - FStringMiddle( + InterpolatedStringMiddle( "__", ), 17..19, @@ -81,7 +80,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "__", ), 27..29, @@ -108,7 +107,7 @@ snapshot_kind: text 36..37, ), ( - FStringMiddle( + InterpolatedStringMiddle( "a\n b\n c\n", ), 37..61, @@ -121,7 +120,7 @@ snapshot_kind: text 61..62, ), ( - FStringMiddle( + InterpolatedStringMiddle( "__", ), 62..64, @@ -148,7 +147,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "__", ), 70..72, @@ -175,7 +174,7 @@ snapshot_kind: text 79..80, ), ( - FStringMiddle( + InterpolatedStringMiddle( "d", ), 80..81, @@ -192,7 +191,7 @@ snapshot_kind: text 82..83, ), ( - FStringMiddle( + InterpolatedStringMiddle( "__", ), 83..85, @@ -219,7 +218,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "__", ), 89..91, @@ -246,7 +245,7 @@ snapshot_kind: text 98..99, ), ( - FStringMiddle( + InterpolatedStringMiddle( "a", ), 99..100, @@ -273,7 +272,7 @@ snapshot_kind: text 111..112, ), ( - FStringMiddle( + InterpolatedStringMiddle( "__", ), 112..114, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_named_expression.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_named_expression.snap index 8f83f01d57..bf3571a289 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_named_expression.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_named_expression.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -28,7 +27,7 @@ snapshot_kind: text 4..5, ), ( - FStringMiddle( + InterpolatedStringMiddle( "=10", ), 5..8, @@ -41,7 +40,7 @@ snapshot_kind: text 8..9, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 9..10, @@ -82,7 +81,7 @@ snapshot_kind: text 18..19, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 19..20, @@ -133,7 +132,7 @@ snapshot_kind: text 30..31, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 31..32, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_nul_char.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_nul_char.snap index 73b431eccc..377acaf33d 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_nul_char.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_nul_char.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\0", ), 2..4, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__nested_t_and_fstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__nested_t_and_fstring.snap new file mode 100644 index 0000000000..f2d59004e5 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__nested_t_and_fstring.snap @@ -0,0 +1,226 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "foo ", + ), + 2..6, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 6..7, + ), + ( + FStringStart, + 7..9, + TokenFlags( + DOUBLE_QUOTES | F_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "bar ", + ), + 9..13, + TokenFlags( + DOUBLE_QUOTES | F_STRING, + ), + ), + ( + Lbrace, + 13..14, + ), + ( + Name( + Name("x"), + ), + 14..15, + ), + ( + Plus, + 16..17, + ), + ( + TStringStart, + 18..20, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 20..21, + ), + ( + Name( + Name("wow"), + ), + 21..24, + ), + ( + Rbrace, + 24..25, + ), + ( + TStringEnd, + 25..26, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Rbrace, + 26..27, + ), + ( + FStringEnd, + 27..28, + TokenFlags( + DOUBLE_QUOTES | F_STRING, + ), + ), + ( + Rbrace, + 28..29, + ), + ( + InterpolatedStringMiddle( + " baz", + ), + 29..33, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 33..34, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + FStringStart, + 35..37, + TokenFlags( + F_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "foo ", + ), + 37..41, + TokenFlags( + F_STRING, + ), + ), + ( + Lbrace, + 41..42, + ), + ( + TStringStart, + 42..44, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "bar", + ), + 44..47, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 47..48, + TokenFlags( + T_STRING, + ), + ), + ( + Exclamation, + 48..49, + ), + ( + Name( + Name("r"), + ), + 49..50, + ), + ( + Rbrace, + 50..51, + ), + ( + InterpolatedStringMiddle( + " some ", + ), + 51..57, + TokenFlags( + F_STRING, + ), + ), + ( + Lbrace, + 57..58, + ), + ( + FStringStart, + 58..60, + TokenFlags( + DOUBLE_QUOTES | F_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "another", + ), + 60..67, + TokenFlags( + DOUBLE_QUOTES | F_STRING, + ), + ), + ( + FStringEnd, + 67..68, + TokenFlags( + DOUBLE_QUOTES | F_STRING, + ), + ), + ( + Rbrace, + 68..69, + ), + ( + FStringEnd, + 69..70, + TokenFlags( + F_STRING, + ), + ), + ( + Newline, + 70..70, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring.snap new file mode 100644 index 0000000000..dde7870ae8 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring.snap @@ -0,0 +1,105 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "normal ", + ), + 2..9, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 9..10, + ), + ( + Name( + Name("foo"), + ), + 10..13, + ), + ( + Rbrace, + 13..14, + ), + ( + InterpolatedStringMiddle( + " {another} ", + ), + 14..27, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 27..28, + ), + ( + Name( + Name("bar"), + ), + 28..31, + ), + ( + Rbrace, + 31..32, + ), + ( + InterpolatedStringMiddle( + " {", + ), + 32..35, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 35..36, + ), + ( + Name( + Name("three"), + ), + 36..41, + ), + ( + Rbrace, + 41..42, + ), + ( + InterpolatedStringMiddle( + "}", + ), + 42..44, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 44..45, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 45..45, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_comments.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_comments.snap new file mode 100644 index 0000000000..5cbdf88979 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_comments.snap @@ -0,0 +1,71 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..4, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\n# not a comment ", + ), + 4..21, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Lbrace, + 21..22, + ), + ( + Comment, + 23..34, + ), + ( + NonLogicalNewline, + 34..35, + ), + ( + Name( + Name("x"), + ), + 39..40, + ), + ( + NonLogicalNewline, + 40..41, + ), + ( + Rbrace, + 41..42, + ), + ( + InterpolatedStringMiddle( + " # not a comment\n", + ), + 42..59, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringEnd, + 59..62, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Newline, + 62..62, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_conversion.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_conversion.snap new file mode 100644 index 0000000000..2e911b3250 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_conversion.snap @@ -0,0 +1,133 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 2..3, + ), + ( + Name( + Name("x"), + ), + 3..4, + ), + ( + Exclamation, + 4..5, + ), + ( + Name( + Name("s"), + ), + 5..6, + ), + ( + Rbrace, + 6..7, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 7..8, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 8..9, + ), + ( + Name( + Name("x"), + ), + 9..10, + ), + ( + Equal, + 10..11, + ), + ( + Exclamation, + 11..12, + ), + ( + Name( + Name("r"), + ), + 12..13, + ), + ( + Rbrace, + 13..14, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 14..15, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 15..16, + ), + ( + Name( + Name("x"), + ), + 16..17, + ), + ( + Colon, + 17..18, + ), + ( + InterpolatedStringMiddle( + ".3f!r", + ), + 18..23, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Rbrace, + 23..24, + ), + ( + InterpolatedStringMiddle( + " {x!r}", + ), + 24..32, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 32..33, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 33..33, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape.snap new file mode 100644 index 0000000000..a69d1b8d97 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape.snap @@ -0,0 +1,86 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\\", + ), + 2..3, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 3..4, + ), + ( + Name( + Name("x"), + ), + 4..5, + ), + ( + Colon, + 5..6, + ), + ( + InterpolatedStringMiddle( + "\\\"\\", + ), + 6..9, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 9..10, + ), + ( + Name( + Name("x"), + ), + 10..11, + ), + ( + Rbrace, + 11..12, + ), + ( + Rbrace, + 12..13, + ), + ( + InterpolatedStringMiddle( + " \\\"\\\"\\\n end", + ), + 13..24, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 24..25, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 25..25, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_braces.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_braces.snap new file mode 100644 index 0000000000..2cf409eae5 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_braces.snap @@ -0,0 +1,133 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\\", + ), + 2..3, + TokenFlags( + T_STRING, + ), + ), + ( + Lbrace, + 3..4, + ), + ( + Name( + Name("foo"), + ), + 4..7, + ), + ( + Rbrace, + 7..8, + ), + ( + TStringEnd, + 8..9, + TokenFlags( + T_STRING, + ), + ), + ( + TStringStart, + 10..12, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\\\\", + ), + 12..14, + TokenFlags( + T_STRING, + ), + ), + ( + Lbrace, + 14..15, + ), + ( + Name( + Name("foo"), + ), + 15..18, + ), + ( + Rbrace, + 18..19, + ), + ( + TStringEnd, + 19..20, + TokenFlags( + T_STRING, + ), + ), + ( + TStringStart, + 21..23, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\\{foo}", + ), + 23..31, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 31..32, + TokenFlags( + T_STRING, + ), + ), + ( + TStringStart, + 33..35, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\\\\{foo}", + ), + 35..44, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 44..45, + TokenFlags( + T_STRING, + ), + ), + ( + Newline, + 45..45, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_raw.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_raw.snap new file mode 100644 index 0000000000..f7b2b27cb8 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_raw.snap @@ -0,0 +1,86 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..3, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + InterpolatedStringMiddle( + "\\", + ), + 3..4, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + Lbrace, + 4..5, + ), + ( + Name( + Name("x"), + ), + 5..6, + ), + ( + Colon, + 6..7, + ), + ( + InterpolatedStringMiddle( + "\\\"\\", + ), + 7..10, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + Lbrace, + 10..11, + ), + ( + Name( + Name("x"), + ), + 11..12, + ), + ( + Rbrace, + 12..13, + ), + ( + Rbrace, + 13..14, + ), + ( + InterpolatedStringMiddle( + " \\\"\\\"\\\n end", + ), + 14..25, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringEnd, + 25..26, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + Newline, + 26..26, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_expression_multiline.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_expression_multiline.snap new file mode 100644 index 0000000000..13894db564 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_expression_multiline.snap @@ -0,0 +1,85 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "first ", + ), + 2..8, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 8..9, + ), + ( + NonLogicalNewline, + 9..10, + ), + ( + Name( + Name("x"), + ), + 14..15, + ), + ( + NonLogicalNewline, + 15..16, + ), + ( + Star, + 24..25, + ), + ( + NonLogicalNewline, + 25..26, + ), + ( + Name( + Name("y"), + ), + 38..39, + ), + ( + NonLogicalNewline, + 39..40, + ), + ( + Rbrace, + 40..41, + ), + ( + InterpolatedStringMiddle( + " second", + ), + 41..48, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 48..49, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 49..49, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_multiline.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_multiline.snap new file mode 100644 index 0000000000..5f4f4496d1 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_multiline.snap @@ -0,0 +1,136 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..4, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\nhello\n world\n", + ), + 4..21, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringEnd, + 21..24, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringStart, + 25..29, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\n world\nhello\n", + ), + 29..46, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringEnd, + 46..49, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringStart, + 50..52, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "some ", + ), + 52..57, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 57..58, + ), + ( + TStringStart, + 58..62, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "multiline\nallowed ", + ), + 62..80, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Lbrace, + 80..81, + ), + ( + Name( + Name("x"), + ), + 81..82, + ), + ( + Rbrace, + 82..83, + ), + ( + TStringEnd, + 83..86, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Rbrace, + 86..87, + ), + ( + InterpolatedStringMiddle( + " string", + ), + 87..94, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 94..95, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 95..95, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode.snap new file mode 100644 index 0000000000..f900cbf95b --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode.snap @@ -0,0 +1,36 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\\N{BULLET} normal \\Nope \\N", + ), + 2..28, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 28..29, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 29..29, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode_raw.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode_raw.snap new file mode 100644 index 0000000000..73b022ad3b --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode_raw.snap @@ -0,0 +1,59 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..3, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + InterpolatedStringMiddle( + "\\N", + ), + 3..5, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + Lbrace, + 5..6, + ), + ( + Name( + Name("BULLET"), + ), + 6..12, + ), + ( + Rbrace, + 12..13, + ), + ( + InterpolatedStringMiddle( + " normal", + ), + 13..20, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringEnd, + 20..21, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + Newline, + 21..21, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_nested.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_nested.snap new file mode 100644 index 0000000000..f786acf7d8 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_nested.snap @@ -0,0 +1,216 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "foo ", + ), + 2..6, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 6..7, + ), + ( + TStringStart, + 7..9, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "bar ", + ), + 9..13, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 13..14, + ), + ( + Name( + Name("x"), + ), + 14..15, + ), + ( + Plus, + 16..17, + ), + ( + TStringStart, + 18..20, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 20..21, + ), + ( + Name( + Name("wow"), + ), + 21..24, + ), + ( + Rbrace, + 24..25, + ), + ( + TStringEnd, + 25..26, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Rbrace, + 26..27, + ), + ( + TStringEnd, + 27..28, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Rbrace, + 28..29, + ), + ( + InterpolatedStringMiddle( + " baz", + ), + 29..33, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 33..34, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 35..37, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "foo ", + ), + 37..41, + TokenFlags( + T_STRING, + ), + ), + ( + Lbrace, + 41..42, + ), + ( + TStringStart, + 42..44, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "bar", + ), + 44..47, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 47..48, + TokenFlags( + T_STRING, + ), + ), + ( + Rbrace, + 48..49, + ), + ( + InterpolatedStringMiddle( + " some ", + ), + 49..55, + TokenFlags( + T_STRING, + ), + ), + ( + Lbrace, + 55..56, + ), + ( + TStringStart, + 56..58, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "another", + ), + 58..65, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 65..66, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Rbrace, + 66..67, + ), + ( + TStringEnd, + 67..68, + TokenFlags( + T_STRING, + ), + ), + ( + Newline, + 68..68, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_parentheses.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_parentheses.snap new file mode 100644 index 0000000000..fcccc68b40 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_parentheses.snap @@ -0,0 +1,209 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 2..3, + ), + ( + Rbrace, + 3..4, + ), + ( + TStringEnd, + 4..5, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 6..8, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "{}", + ), + 8..12, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 12..13, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 14..16, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + " ", + ), + 16..17, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 17..18, + ), + ( + Rbrace, + 18..19, + ), + ( + TStringEnd, + 19..20, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 21..23, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "{", + ), + 23..25, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 25..26, + ), + ( + Rbrace, + 26..27, + ), + ( + InterpolatedStringMiddle( + "}", + ), + 27..29, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 29..30, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 31..33, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "{{}}", + ), + 33..41, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 41..42, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 43..45, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + " ", + ), + 45..46, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 46..47, + ), + ( + Rbrace, + 47..48, + ), + ( + InterpolatedStringMiddle( + " {} {", + ), + 48..56, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 56..57, + ), + ( + Rbrace, + 57..58, + ), + ( + InterpolatedStringMiddle( + "} {{}} ", + ), + 58..71, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 71..72, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 72..72, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_prefix.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_prefix.snap new file mode 100644 index 0000000000..8a285e58fb --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_prefix.snap @@ -0,0 +1,153 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 2..3, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 4..6, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 6..7, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 8..11, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringEnd, + 11..12, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringStart, + 13..16, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringEnd, + 16..17, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringStart, + 18..21, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE, + ), + ), + ( + TStringEnd, + 21..22, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE, + ), + ), + ( + TStringStart, + 23..26, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE, + ), + ), + ( + TStringEnd, + 26..27, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE, + ), + ), + ( + TStringStart, + 28..31, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringEnd, + 31..32, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringStart, + 33..36, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringEnd, + 36..37, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringStart, + 38..41, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE, + ), + ), + ( + TStringEnd, + 41..42, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE, + ), + ), + ( + TStringStart, + 43..46, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE, + ), + ), + ( + TStringEnd, + 46..47, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE, + ), + ), + ( + Newline, + 47..47, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_mac_eol.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_mac_eol.snap new file mode 100644 index 0000000000..515c486b9b --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_mac_eol.snap @@ -0,0 +1,36 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: tstring_single_quote_escape_eol(MAC_EOL) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "text \\\r more text", + ), + 2..19, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 19..20, + TokenFlags( + T_STRING, + ), + ), + ( + Newline, + 20..20, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_unix_eol.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_unix_eol.snap new file mode 100644 index 0000000000..eed02e6ab6 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_unix_eol.snap @@ -0,0 +1,36 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: tstring_single_quote_escape_eol(UNIX_EOL) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "text \\\n more text", + ), + 2..19, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 19..20, + TokenFlags( + T_STRING, + ), + ), + ( + Newline, + 20..20, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_windows_eol.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_windows_eol.snap new file mode 100644 index 0000000000..424092796d --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_windows_eol.snap @@ -0,0 +1,36 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: tstring_single_quote_escape_eol(WINDOWS_EOL) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "text \\\r\n more text", + ), + 2..20, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 20..21, + TokenFlags( + T_STRING, + ), + ), + ( + Newline, + 21..21, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_format_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_format_spec.snap new file mode 100644 index 0000000000..d8760b764d --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_format_spec.snap @@ -0,0 +1,289 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 2..3, + ), + ( + Name( + Name("foo"), + ), + 3..6, + ), + ( + Colon, + 6..7, + ), + ( + Rbrace, + 7..8, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 8..9, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 9..10, + ), + ( + Name( + Name("x"), + ), + 10..11, + ), + ( + Equal, + 11..12, + ), + ( + Exclamation, + 12..13, + ), + ( + Name( + Name("s"), + ), + 13..14, + ), + ( + Colon, + 14..15, + ), + ( + InterpolatedStringMiddle( + ".3f", + ), + 15..18, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Rbrace, + 18..19, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 19..20, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 20..21, + ), + ( + Name( + Name("x"), + ), + 21..22, + ), + ( + Colon, + 22..23, + ), + ( + InterpolatedStringMiddle( + ".", + ), + 23..24, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 24..25, + ), + ( + Name( + Name("y"), + ), + 25..26, + ), + ( + Rbrace, + 26..27, + ), + ( + InterpolatedStringMiddle( + "f", + ), + 27..28, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Rbrace, + 28..29, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 29..30, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 30..31, + ), + ( + String( + "", + ), + 31..33, + ), + ( + Colon, + 33..34, + ), + ( + InterpolatedStringMiddle( + "*^", + ), + 34..36, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 36..37, + ), + ( + Int( + 1, + ), + 37..38, + ), + ( + Colon, + 38..39, + ), + ( + Lbrace, + 39..40, + ), + ( + Int( + 1, + ), + 40..41, + ), + ( + Rbrace, + 41..42, + ), + ( + Rbrace, + 42..43, + ), + ( + Rbrace, + 43..44, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 44..45, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 45..46, + ), + ( + Name( + Name("x"), + ), + 46..47, + ), + ( + Colon, + 47..48, + ), + ( + Lbrace, + 48..49, + ), + ( + Lbrace, + 49..50, + ), + ( + Int( + 1, + ), + 50..51, + ), + ( + Rbrace, + 51..52, + ), + ( + Dot, + 52..53, + ), + ( + Name( + Name("pop"), + ), + 53..56, + ), + ( + Lpar, + 56..57, + ), + ( + Rpar, + 57..58, + ), + ( + Rbrace, + 58..59, + ), + ( + Rbrace, + 59..60, + ), + ( + TStringEnd, + 60..61, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 61..61, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_ipy_escape_command.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_ipy_escape_command.snap new file mode 100644 index 0000000000..739930ef42 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_ipy_escape_command.snap @@ -0,0 +1,63 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "foo ", + ), + 2..6, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 6..7, + ), + ( + Exclamation, + 7..8, + ), + ( + Name( + Name("pwd"), + ), + 8..11, + ), + ( + Rbrace, + 11..12, + ), + ( + InterpolatedStringMiddle( + " bar", + ), + 12..16, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 16..17, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 17..17, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_lambda_expression.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_lambda_expression.snap new file mode 100644 index 0000000000..679142f387 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_lambda_expression.snap @@ -0,0 +1,125 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 2..3, + ), + ( + Lambda, + 3..9, + ), + ( + Name( + Name("x"), + ), + 10..11, + ), + ( + Colon, + 11..12, + ), + ( + Lbrace, + 12..13, + ), + ( + Name( + Name("x"), + ), + 13..14, + ), + ( + Rbrace, + 14..15, + ), + ( + Rbrace, + 15..16, + ), + ( + TStringEnd, + 16..17, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 17..18, + ), + ( + TStringStart, + 18..20, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 20..21, + ), + ( + Lpar, + 21..22, + ), + ( + Lambda, + 22..28, + ), + ( + Name( + Name("x"), + ), + 29..30, + ), + ( + Colon, + 30..31, + ), + ( + Lbrace, + 31..32, + ), + ( + Name( + Name("x"), + ), + 32..33, + ), + ( + Rbrace, + 33..34, + ), + ( + Rpar, + 34..35, + ), + ( + Rbrace, + 35..36, + ), + ( + TStringEnd, + 36..37, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 37..37, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_multiline_format_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_multiline_format_spec.snap new file mode 100644 index 0000000000..1525f2a0eb --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_multiline_format_spec.snap @@ -0,0 +1,295 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..4, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "__", + ), + 4..6, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Lbrace, + 6..7, + ), + ( + NonLogicalNewline, + 7..8, + ), + ( + Name( + Name("x"), + ), + 12..13, + ), + ( + Colon, + 13..14, + ), + ( + InterpolatedStringMiddle( + "d\n", + ), + 14..16, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Rbrace, + 16..17, + ), + ( + InterpolatedStringMiddle( + "__", + ), + 17..19, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringEnd, + 19..22, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Newline, + 22..23, + ), + ( + TStringStart, + 23..27, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "__", + ), + 27..29, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Lbrace, + 29..30, + ), + ( + NonLogicalNewline, + 30..31, + ), + ( + Name( + Name("x"), + ), + 35..36, + ), + ( + Colon, + 36..37, + ), + ( + InterpolatedStringMiddle( + "a\n b\n c\n", + ), + 37..61, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Rbrace, + 61..62, + ), + ( + InterpolatedStringMiddle( + "__", + ), + 62..64, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringEnd, + 64..67, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Newline, + 67..68, + ), + ( + TStringStart, + 68..70, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "__", + ), + 70..72, + TokenFlags( + T_STRING, + ), + ), + ( + Lbrace, + 72..73, + ), + ( + NonLogicalNewline, + 73..74, + ), + ( + Name( + Name("x"), + ), + 78..79, + ), + ( + Colon, + 79..80, + ), + ( + InterpolatedStringMiddle( + "d", + ), + 80..81, + TokenFlags( + T_STRING, + ), + ), + ( + NonLogicalNewline, + 81..82, + ), + ( + Rbrace, + 82..83, + ), + ( + InterpolatedStringMiddle( + "__", + ), + 83..85, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 85..86, + TokenFlags( + T_STRING, + ), + ), + ( + Newline, + 86..87, + ), + ( + TStringStart, + 87..89, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "__", + ), + 89..91, + TokenFlags( + T_STRING, + ), + ), + ( + Lbrace, + 91..92, + ), + ( + NonLogicalNewline, + 92..93, + ), + ( + Name( + Name("x"), + ), + 97..98, + ), + ( + Colon, + 98..99, + ), + ( + InterpolatedStringMiddle( + "a", + ), + 99..100, + TokenFlags( + T_STRING, + ), + ), + ( + NonLogicalNewline, + 100..101, + ), + ( + Name( + Name("b"), + ), + 109..110, + ), + ( + NonLogicalNewline, + 110..111, + ), + ( + Rbrace, + 111..112, + ), + ( + InterpolatedStringMiddle( + "__", + ), + 112..114, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 114..115, + TokenFlags( + T_STRING, + ), + ), + ( + Newline, + 115..116, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_named_expression.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_named_expression.snap new file mode 100644 index 0000000000..7ab9d20f3f --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_named_expression.snap @@ -0,0 +1,187 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 2..3, + ), + ( + Name( + Name("x"), + ), + 3..4, + ), + ( + Colon, + 4..5, + ), + ( + InterpolatedStringMiddle( + "=10", + ), + 5..8, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Rbrace, + 8..9, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 9..10, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 10..11, + ), + ( + Lpar, + 11..12, + ), + ( + Name( + Name("x"), + ), + 12..13, + ), + ( + ColonEqual, + 13..15, + ), + ( + Int( + 10, + ), + 15..17, + ), + ( + Rpar, + 17..18, + ), + ( + Rbrace, + 18..19, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 19..20, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 20..21, + ), + ( + Name( + Name("x"), + ), + 21..22, + ), + ( + Comma, + 22..23, + ), + ( + Lbrace, + 23..24, + ), + ( + Name( + Name("y"), + ), + 24..25, + ), + ( + ColonEqual, + 25..27, + ), + ( + Int( + 10, + ), + 27..29, + ), + ( + Rbrace, + 29..30, + ), + ( + Rbrace, + 30..31, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 31..32, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 32..33, + ), + ( + Lsqb, + 33..34, + ), + ( + Name( + Name("x"), + ), + 34..35, + ), + ( + ColonEqual, + 35..37, + ), + ( + Int( + 10, + ), + 37..39, + ), + ( + Rsqb, + 39..40, + ), + ( + Rbrace, + 40..41, + ), + ( + TStringEnd, + 41..42, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 42..42, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_nul_char.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_nul_char.snap new file mode 100644 index 0000000000..483ea5d6ec --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_nul_char.snap @@ -0,0 +1,36 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\\0", + ), + 2..4, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 4..5, + TokenFlags( + T_STRING, + ), + ), + ( + Newline, + 5..5, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_constant_range.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_constant_range.snap index 99985450fd..78e450c4e2 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_constant_range.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_constant_range.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -17,13 +16,13 @@ snapshot_kind: text range: 0..22, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 2..5, value: "aaa", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 5..10, expression: Name( ExprName { @@ -38,13 +37,13 @@ snapshot_kind: text }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 10..13, value: "ccc", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 13..18, expression: Name( ExprName { @@ -59,7 +58,7 @@ snapshot_kind: text }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 18..21, value: "eee", }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_character.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_character.snap index 2721f57e77..02db608ab1 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_character.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_character.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -17,13 +16,13 @@ snapshot_kind: text range: 0..8, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 2..4, value: "\\", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 4..7, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_newline.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_newline.snap index 44b3a5dcad..9482530d4a 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_newline.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_newline.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -17,13 +16,13 @@ snapshot_kind: text range: 0..8, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 2..4, value: "\n", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 4..7, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_line_continuation.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_line_continuation.snap index 898ff2d347..6ccd6f466d 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_line_continuation.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_line_continuation.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -17,13 +16,13 @@ snapshot_kind: text range: 0..9, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 3..5, value: "\\\n", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 5..8, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base.snap index 1f21cd2286..840873127a 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..10, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..9, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base_more.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base_more.snap index 9a6ed32258..3d5f1e79c0 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base_more.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base_more.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -17,13 +16,13 @@ snapshot_kind: text range: 0..38, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 2..6, value: "mix ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 6..13, expression: Name( ExprName { @@ -43,13 +42,13 @@ snapshot_kind: text }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 13..28, value: " with text and ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 28..37, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_format.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_format.snap index 8c786dbbd1..d2063a00ec 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_format.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_format.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..14, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..13, expression: Name( ExprName { @@ -34,11 +33,11 @@ snapshot_kind: text ), conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 9..12, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 9..12, value: ">10", }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_unescaped_newline.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_unescaped_newline.snap index eedba2c8bc..98be2ba27a 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_unescaped_newline.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_unescaped_newline.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -17,13 +16,13 @@ snapshot_kind: text range: 0..11, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 4..5, value: "\n", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 5..8, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_fstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_fstring.snap index ffd0191500..1a57e2606a 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_fstring.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_fstring.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_tstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_tstring.snap new file mode 100644 index 0000000000..3ccfef4404 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_tstring.snap @@ -0,0 +1,31 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..3, + value: TString( + ExprTString { + range: 0..3, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..3, + elements: [], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_1.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_1.snap index 983da17da4..e43a1b109c 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_1.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_1.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -29,7 +28,7 @@ snapshot_kind: text range: 9..17, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 11..16, value: "world", }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_2.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_2.snap index 983da17da4..e43a1b109c 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_2.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_2.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -29,7 +28,7 @@ snapshot_kind: text range: 9..17, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 11..16, value: "world", }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_3.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_3.snap index 8479de9437..a9f5a2c31f 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_3.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_3.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -29,13 +28,13 @@ snapshot_kind: text range: 9..22, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 11..16, value: "world", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 16..21, expression: StringLiteral( ExprStringLiteral { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_4.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_4.snap index 56a819bd71..6b348cab6d 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_4.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_4.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -29,13 +28,13 @@ snapshot_kind: text range: 9..22, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 11..16, value: "world", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 16..21, expression: StringLiteral( ExprStringLiteral { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_1.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_1.snap new file mode 100644 index 0000000000..c1432f9b03 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_1.snap @@ -0,0 +1,58 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..18, + value: TString( + ExprTString { + range: 0..18, + value: TStringValue { + inner: Concatenated( + [ + FString( + FString { + range: 0..9, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 2..8, + value: "Hello ", + }, + ), + ], + flags: FStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 10..18, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 12..17, + value: "world", + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_2.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_2.snap new file mode 100644 index 0000000000..b3a9d72180 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_2.snap @@ -0,0 +1,69 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..22, + value: TString( + ExprTString { + range: 0..22, + value: TStringValue { + inner: Concatenated( + [ + FString( + FString { + range: 0..9, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 2..8, + value: "Hello ", + }, + ), + ], + flags: FStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 10..18, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 12..17, + value: "world", + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 19..22, + value: "!", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring.snap index 4ec8dc27be..b4fbc87730 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..18, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..5, expression: Name( ExprName { @@ -31,8 +30,8 @@ snapshot_kind: text format_spec: None, }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 5..10, expression: Name( ExprName { @@ -47,7 +46,7 @@ snapshot_kind: text }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 10..17, value: "{foo}", }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_equals.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_equals.snap index 58821196bd..57e5d8296c 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_equals.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_equals.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..13, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..12, expression: Compare( ExprCompare { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_concatenation_string_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_concatenation_string_spec.snap index 9e5b4a2fc8..f0f88bc994 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_concatenation_string_spec.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_concatenation_string_spec.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..16, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..15, expression: Name( ExprName { @@ -29,11 +28,11 @@ snapshot_kind: text debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 7..14, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 7..14, expression: StringLiteral( ExprStringLiteral { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_spec.snap index 28e6e4a2a1..5b4aebba06 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_spec.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_spec.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..15, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..14, expression: Name( ExprName { @@ -29,11 +28,11 @@ snapshot_kind: text debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 7..13, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 7..13, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_string_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_string_spec.snap index 9dae288239..c9ab599e07 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_string_spec.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_string_spec.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..13, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..12, expression: Name( ExprName { @@ -29,11 +28,11 @@ snapshot_kind: text debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 7..11, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 7..11, expression: StringLiteral( ExprStringLiteral { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_equals.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_equals.snap index d5884e9d75..b631befca6 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_equals.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_equals.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..11, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..10, expression: Compare( ExprCompare { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_nested_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_nested_spec.snap index 738b731a41..93308bd0ca 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_nested_spec.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_nested_spec.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..13, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..12, expression: Name( ExprName { @@ -29,11 +28,11 @@ snapshot_kind: text debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 7..11, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 7..11, value: "spec", }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_prec_space.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_prec_space.snap index e1d2941dc5..2d72726265 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_prec_space.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_prec_space.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..10, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..9, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_trailing_space.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_trailing_space.snap index e5857594c1..e11b44be8a 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_trailing_space.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_trailing_space.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..10, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..9, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_yield_expr.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_yield_expr.snap index 3dca1cc84b..bcd14124f1 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_yield_expr.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_yield_expr.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..10, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..9, expression: Yield( ExprYield { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_1.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_1.snap new file mode 100644 index 0000000000..cc79ec38df --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_1.snap @@ -0,0 +1,51 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..17, + value: TString( + ExprTString { + range: 0..17, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 0..8, + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 9..17, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 11..16, + value: "world", + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_2.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_2.snap new file mode 100644 index 0000000000..cc79ec38df --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_2.snap @@ -0,0 +1,51 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..17, + value: TString( + ExprTString { + range: 0..17, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 0..8, + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 9..17, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 11..16, + value: "world", + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_3.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_3.snap new file mode 100644 index 0000000000..080c41beec --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_3.snap @@ -0,0 +1,77 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..22, + value: TString( + ExprTString { + range: 0..22, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 0..8, + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 9..22, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 11..16, + value: "world", + }, + ), + Interpolation( + InterpolatedElement { + range: 16..21, + expression: StringLiteral( + ExprStringLiteral { + range: 17..20, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 17..20, + value: "!", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_4.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_4.snap new file mode 100644 index 0000000000..7d178f9569 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_4.snap @@ -0,0 +1,88 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..31, + value: TString( + ExprTString { + range: 0..31, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 0..8, + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 9..22, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 11..16, + value: "world", + }, + ), + Interpolation( + InterpolatedElement { + range: 16..21, + expression: StringLiteral( + ExprStringLiteral { + range: 17..20, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 17..20, + value: "!", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 23..31, + value: "again!", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring.snap new file mode 100644 index 0000000000..999de0e0e8 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring.snap @@ -0,0 +1,68 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..18, + value: TString( + ExprTString { + range: 0..18, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..18, + elements: [ + Interpolation( + InterpolatedElement { + range: 2..5, + expression: Name( + ExprName { + range: 3..4, + id: Name("a"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Interpolation( + InterpolatedElement { + range: 5..10, + expression: Name( + ExprName { + range: 7..8, + id: Name("b"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 10..17, + value: "{foo}", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_equals.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_equals.snap new file mode 100644 index 0000000000..2618c0abf7 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_equals.snap @@ -0,0 +1,66 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..13, + value: TString( + ExprTString { + range: 0..13, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..13, + elements: [ + Interpolation( + InterpolatedElement { + range: 2..12, + expression: Compare( + ExprCompare { + range: 3..11, + left: NumberLiteral( + ExprNumberLiteral { + range: 3..5, + value: Int( + 42, + ), + }, + ), + ops: [ + Eq, + ], + comparators: [ + NumberLiteral( + ExprNumberLiteral { + range: 9..11, + value: Int( + 42, + ), + }, + ), + ], + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_concatenation_string_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_concatenation_string_spec.snap new file mode 100644 index 0000000000..26719dcb0a --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_concatenation_string_spec.snap @@ -0,0 +1,93 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..16, + value: TString( + ExprTString { + range: 0..16, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..16, + elements: [ + Interpolation( + InterpolatedElement { + range: 2..15, + expression: Name( + ExprName { + range: 3..6, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 7..14, + elements: [ + Interpolation( + InterpolatedElement { + range: 7..14, + expression: StringLiteral( + ExprStringLiteral { + range: 8..13, + value: StringLiteralValue { + inner: Concatenated( + ConcatenatedStringLiteral { + strings: [ + StringLiteral { + range: 8..10, + value: "", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + StringLiteral { + range: 11..13, + value: "", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ], + value: "", + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_spec.snap new file mode 100644 index 0000000000..0bd592171b --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_spec.snap @@ -0,0 +1,68 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..15, + value: TString( + ExprTString { + range: 0..15, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..15, + elements: [ + Interpolation( + InterpolatedElement { + range: 2..14, + expression: Name( + ExprName { + range: 3..6, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 7..13, + elements: [ + Interpolation( + InterpolatedElement { + range: 7..13, + expression: Name( + ExprName { + range: 8..12, + id: Name("spec"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_string_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_string_spec.snap new file mode 100644 index 0000000000..cfa89174c8 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_string_spec.snap @@ -0,0 +1,79 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..13, + value: TString( + ExprTString { + range: 0..13, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..13, + elements: [ + Interpolation( + InterpolatedElement { + range: 2..12, + expression: Name( + ExprName { + range: 3..6, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 7..11, + elements: [ + Interpolation( + InterpolatedElement { + range: 7..11, + expression: StringLiteral( + ExprStringLiteral { + range: 8..10, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 8..10, + value: "", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_equals.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_equals.snap new file mode 100644 index 0000000000..96bc26e6fc --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_equals.snap @@ -0,0 +1,66 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..11, + value: TString( + ExprTString { + range: 0..11, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..11, + elements: [ + Interpolation( + InterpolatedElement { + range: 2..10, + expression: Compare( + ExprCompare { + range: 3..9, + left: NumberLiteral( + ExprNumberLiteral { + range: 3..4, + value: Int( + 1, + ), + }, + ), + ops: [ + NotEq, + ], + comparators: [ + NumberLiteral( + ExprNumberLiteral { + range: 8..9, + value: Int( + 2, + ), + }, + ), + ], + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_nested_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_nested_spec.snap new file mode 100644 index 0000000000..b77e560ea2 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_nested_spec.snap @@ -0,0 +1,59 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..13, + value: TString( + ExprTString { + range: 0..13, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..13, + elements: [ + Interpolation( + InterpolatedElement { + range: 2..12, + expression: Name( + ExprName { + range: 3..6, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 7..11, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 7..11, + value: "spec", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_prec_space.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_prec_space.snap new file mode 100644 index 0000000000..dc1558c8d2 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_prec_space.snap @@ -0,0 +1,52 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..10, + value: TString( + ExprTString { + range: 0..10, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..10, + elements: [ + Interpolation( + InterpolatedElement { + range: 2..9, + expression: Name( + ExprName { + range: 3..4, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: " =", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_trailing_space.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_trailing_space.snap new file mode 100644 index 0000000000..a6c8b8849b --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_trailing_space.snap @@ -0,0 +1,52 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..10, + value: TString( + ExprTString { + range: 0..10, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..10, + elements: [ + Interpolation( + InterpolatedElement { + range: 2..9, + expression: Name( + ExprName { + range: 3..4, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "= ", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_yield_expr.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_yield_expr.snap new file mode 100644 index 0000000000..7693375a34 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_yield_expr.snap @@ -0,0 +1,46 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..10, + value: TString( + ExprTString { + range: 0..10, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..10, + elements: [ + Interpolation( + InterpolatedElement { + range: 2..9, + expression: Yield( + ExprYield { + range: 3..8, + value: None, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_1.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_1.snap index 2d864494e5..31a9098fde 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_1.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_1.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -29,7 +28,7 @@ snapshot_kind: text range: 10..18, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 12..17, value: "world", }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_2.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_2.snap index e3bafd8a1f..04c02e6462 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_2.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_2.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -29,7 +28,7 @@ snapshot_kind: text range: 10..18, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 12..17, value: "world", }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_1.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_1.snap new file mode 100644 index 0000000000..0fd3e39703 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_1.snap @@ -0,0 +1,51 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..18, + value: TString( + ExprTString { + range: 0..18, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 0..9, + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Unicode, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 10..18, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 12..17, + value: "world", + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_2.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_2.snap new file mode 100644 index 0000000000..ae5721a931 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_2.snap @@ -0,0 +1,62 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..22, + value: TString( + ExprTString { + range: 0..22, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 0..9, + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Unicode, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 10..18, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 12..17, + value: "world", + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 19..22, + value: "!", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_fstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_fstring.snap index ff531b735e..d835921a9a 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_fstring.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_fstring.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..7, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 3..6, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_tstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_tstring.snap new file mode 100644 index 0000000000..5f16d7cc6b --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_tstring.snap @@ -0,0 +1,49 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..7, + value: TString( + ExprTString { + range: 0..7, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..7, + elements: [ + Interpolation( + InterpolatedElement { + range: 3..6, + expression: Name( + ExprName { + range: 4..5, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Raw { + uppercase_r: false, + }, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_fstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_fstring.snap index 1d0ed68bc3..2c0adae7d1 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_fstring.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_fstring.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..11, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 5..8, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_tstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_tstring.snap new file mode 100644 index 0000000000..7a383de39c --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_tstring.snap @@ -0,0 +1,49 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..11, + value: TString( + ExprTString { + range: 0..11, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..11, + elements: [ + Interpolation( + InterpolatedElement { + range: 5..8, + expression: Name( + ExprName { + range: 6..7, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Raw { + uppercase_r: false, + }, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_constant_range.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_constant_range.snap new file mode 100644 index 0000000000..6227dea0fe --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_constant_range.snap @@ -0,0 +1,80 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..22, + value: TString( + ExprTString { + range: 0..22, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..22, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 2..5, + value: "aaa", + }, + ), + Interpolation( + InterpolatedElement { + range: 5..10, + expression: Name( + ExprName { + range: 6..9, + id: Name("bbb"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 10..13, + value: "ccc", + }, + ), + Interpolation( + InterpolatedElement { + range: 13..18, + expression: Name( + ExprName { + range: 14..17, + id: Name("ddd"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 18..21, + value: "eee", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_character.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_character.snap new file mode 100644 index 0000000000..b72088efc8 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_character.snap @@ -0,0 +1,53 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..8, + value: TString( + ExprTString { + range: 0..8, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..8, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 2..4, + value: "\\", + }, + ), + Interpolation( + InterpolatedElement { + range: 4..7, + expression: Name( + ExprName { + range: 5..6, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_newline.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_newline.snap new file mode 100644 index 0000000000..d34b25231f --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_newline.snap @@ -0,0 +1,53 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..8, + value: TString( + ExprTString { + range: 0..8, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..8, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 2..4, + value: "\n", + }, + ), + Interpolation( + InterpolatedElement { + range: 4..7, + expression: Name( + ExprName { + range: 5..6, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_line_continuation.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_line_continuation.snap new file mode 100644 index 0000000000..396755f985 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_line_continuation.snap @@ -0,0 +1,55 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..9, + value: TString( + ExprTString { + range: 0..9, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..9, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 3..5, + value: "\\\n", + }, + ), + Interpolation( + InterpolatedElement { + range: 5..8, + expression: Name( + ExprName { + range: 6..7, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Raw { + uppercase_r: false, + }, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base.snap new file mode 100644 index 0000000000..99398a48b1 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base.snap @@ -0,0 +1,52 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..10, + value: TString( + ExprTString { + range: 0..10, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..10, + elements: [ + Interpolation( + InterpolatedElement { + range: 2..9, + expression: Name( + ExprName { + range: 3..7, + id: Name("user"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base_more.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base_more.snap new file mode 100644 index 0000000000..a745f0f639 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base_more.snap @@ -0,0 +1,84 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..38, + value: TString( + ExprTString { + range: 0..38, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..38, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 2..6, + value: "mix ", + }, + ), + Interpolation( + InterpolatedElement { + range: 6..13, + expression: Name( + ExprName { + range: 7..11, + id: Name("user"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 13..28, + value: " with text and ", + }, + ), + Interpolation( + InterpolatedElement { + range: 28..37, + expression: Name( + ExprName { + range: 29..35, + id: Name("second"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_format.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_format.snap new file mode 100644 index 0000000000..a401f11e74 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_format.snap @@ -0,0 +1,64 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..14, + value: TString( + ExprTString { + range: 0..14, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..14, + elements: [ + Interpolation( + InterpolatedElement { + range: 2..13, + expression: Name( + ExprName { + range: 3..7, + id: Name("user"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 9..12, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 9..12, + value: ">10", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_unescaped_newline.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_unescaped_newline.snap new file mode 100644 index 0000000000..b14e366fb4 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_unescaped_newline.snap @@ -0,0 +1,53 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..11, + value: TString( + ExprTString { + range: 0..11, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..11, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 4..5, + value: "\n", + }, + ), + Interpolation( + InterpolatedElement { + range: 5..8, + expression: Name( + ExprName { + range: 6..7, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/string.rs b/crates/ruff_python_parser/src/string.rs index 7ae24798e3..6642fbc25a 100644 --- a/crates/ruff_python_parser/src/string.rs +++ b/crates/ruff_python_parser/src/string.rs @@ -1,17 +1,22 @@ //! Parsing of string literals, bytes literals, and implicit string concatenation. use bstr::ByteSlice; +use std::fmt; use ruff_python_ast::{self as ast, AnyStringFlags, Expr, StringFlags}; use ruff_text_size::{Ranged, TextRange, TextSize}; -use crate::error::{LexicalError, LexicalErrorType}; +use crate::{ + TokenKind, + error::{LexicalError, LexicalErrorType}, +}; #[derive(Debug)] pub(crate) enum StringType { Str(ast::StringLiteral), Bytes(ast::BytesLiteral), FString(ast::FString), + TString(ast::TString), } impl Ranged for StringType { @@ -20,6 +25,7 @@ impl Ranged for StringType { Self::Str(node) => node.range(), Self::Bytes(node) => node.range(), Self::FString(node) => node.range(), + Self::TString(node) => node.range(), } } } @@ -30,6 +36,48 @@ impl From for Expr { StringType::Str(node) => Expr::from(node), StringType::Bytes(node) => Expr::from(node), StringType::FString(node) => Expr::from(node), + StringType::TString(node) => Expr::from(node), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub(crate) enum InterpolatedStringKind { + FString, + TString, +} + +impl InterpolatedStringKind { + #[inline] + pub(crate) const fn start_token(self) -> TokenKind { + match self { + InterpolatedStringKind::FString => TokenKind::FStringStart, + InterpolatedStringKind::TString => TokenKind::TStringStart, + } + } + + #[inline] + pub(crate) const fn middle_token(self) -> TokenKind { + match self { + InterpolatedStringKind::FString => TokenKind::FStringMiddle, + InterpolatedStringKind::TString => TokenKind::TStringMiddle, + } + } + + #[inline] + pub(crate) const fn end_token(self) -> TokenKind { + match self { + InterpolatedStringKind::FString => TokenKind::FStringEnd, + InterpolatedStringKind::TString => TokenKind::TStringEnd, + } + } +} + +impl fmt::Display for InterpolatedStringKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + InterpolatedStringKind::FString => f.write_str("f-string"), + InterpolatedStringKind::TString => f.write_str("t-string"), } } } @@ -231,10 +279,12 @@ impl StringParser { Ok(Some(EscapedChar::Literal(new_char))) } - fn parse_fstring_middle(mut self) -> Result { - // Fast-path: if the f-string doesn't contain any escape sequences, return the literal. + fn parse_interpolated_string_middle( + mut self, + ) -> Result { + // Fast-path: if the f-string or t-string doesn't contain any escape sequences, return the literal. let Some(mut index) = memchr::memchr3(b'{', b'}', b'\\', self.source.as_bytes()) else { - return Ok(ast::FStringLiteralElement { + return Ok(ast::InterpolatedStringLiteralElement { value: self.source, range: self.range, }); @@ -249,7 +299,7 @@ impl StringParser { // Add the escaped character to the string. match &self.source.as_bytes()[self.cursor - 1] { - // If there are any curly braces inside a `FStringMiddle` token, + // If there are any curly braces inside a `F/TStringMiddle` token, // then they were escaped (i.e. `{{` or `}}`). This means that // we need increase the location by 2 instead of 1. b'{' => { @@ -260,7 +310,7 @@ impl StringParser { self.offset += TextSize::from(1); value.push('}'); } - // We can encounter a `\` as the last character in a `FStringMiddle` + // We can encounter a `\` as the last character in a `F/TStringMiddle` // token which is valid in this context. For example, // // ```python @@ -268,7 +318,7 @@ impl StringParser { // # ^ ^^ ^ // ``` // - // Here, the `FStringMiddle` token content will be "\" and " \" + // Here, the `F/TStringMiddle` token content will be "\" and " \" // which is invalid if we look at the content in isolation: // // ```python @@ -276,7 +326,7 @@ impl StringParser { // ``` // // However, the content is syntactically valid in the context of - // the f-string because it's a substring of the entire f-string. + // the f/t-string because it's a substring of the entire f/t-string. // This is still an invalid escape sequence, but we don't want to // raise a syntax error as is done by the CPython parser. It might // be supported in the future, refer to point 3: https://peps.python.org/pep-0701/#rejected-ideas @@ -311,7 +361,7 @@ impl StringParser { index = next_index; } - Ok(ast::FStringLiteralElement { + Ok(ast::InterpolatedStringLiteralElement { value: value.into_boxed_str(), range: self.range, }) @@ -458,12 +508,12 @@ pub(crate) fn parse_string_literal( } // TODO(dhruvmanila): Move this to the new parser -pub(crate) fn parse_fstring_literal_element( +pub(crate) fn parse_interpolated_string_literal_element( source: Box, flags: AnyStringFlags, range: TextRange, -) -> Result { - StringParser::new(source, flags, range.start(), range).parse_fstring_middle() +) -> Result { + StringParser::new(source, flags, range.start(), range).parse_interpolated_string_middle() } #[cfg(test)] @@ -471,7 +521,7 @@ mod tests { use ruff_python_ast::Suite; use crate::error::LexicalErrorType; - use crate::{FStringErrorType, ParseError, ParseErrorType, Parsed, parse_module}; + use crate::{InterpolatedStringErrorType, ParseError, ParseErrorType, Parsed, parse_module}; const WINDOWS_EOL: &str = "\r\n"; const MAC_EOL: &str = "\r"; @@ -553,7 +603,7 @@ mod tests { insta::assert_debug_snapshot!(suite); } - fn parse_fstring_error(source: &str) -> FStringErrorType { + fn parse_fstring_error(source: &str) -> InterpolatedStringErrorType { parse_suite(source) .map_err(|e| match e.error { ParseErrorType::Lexical(LexicalErrorType::FStringError(e)) => e, @@ -565,7 +615,7 @@ mod tests { #[test] fn test_parse_invalid_fstring() { - use FStringErrorType::{InvalidConversionFlag, LambdaWithoutParentheses}; + use InterpolatedStringErrorType::{InvalidConversionFlag, LambdaWithoutParentheses}; assert_eq!(parse_fstring_error(r#"f"{5!x}""#), InvalidConversionFlag); assert_eq!( @@ -616,6 +666,118 @@ mod tests { insta::assert_debug_snapshot!(suite); } + #[test] + fn test_parse_tstring() { + let source = r#"t"{a}{ b }{{foo}}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_nested_spec() { + let source = r#"t"{foo:{spec}}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_not_nested_spec() { + let source = r#"t"{foo:spec}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_empty_tstring() { + let source = r#"t"""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_parse_self_documenting_base() { + let source = r#"t"{user=}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_parse_self_documenting_base_more() { + let source = r#"t"mix {user=} with text and {second=}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_parse_self_documenting_format() { + let source = r#"t"{user=:>10}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + fn parse_tstring_error(source: &str) -> InterpolatedStringErrorType { + parse_suite(source) + .map_err(|e| match e.error { + ParseErrorType::Lexical(LexicalErrorType::TStringError(e)) => e, + ParseErrorType::TStringError(e) => e, + e => unreachable!("Expected TStringError: {:?}", e), + }) + .expect_err("Expected error") + } + + #[test] + fn test_parse_invalid_tstring() { + use InterpolatedStringErrorType::{InvalidConversionFlag, LambdaWithoutParentheses}; + + assert_eq!(parse_tstring_error(r#"t"{5!x}""#), InvalidConversionFlag); + assert_eq!( + parse_tstring_error("t'{lambda x:{x}}'"), + LambdaWithoutParentheses + ); + // NOTE: The parser produces the `LambdaWithoutParentheses` for this case, but + // since the parser only return the first error to maintain compatibility with + // the rest of the codebase, this test case fails. The `LambdaWithoutParentheses` + // error appears after the unexpected `tStringMiddle` token, which is between the + // `:` and the `{`. + // assert_eq!(parse_tstring_error("f'{lambda x: {x}}'"), LambdaWithoutParentheses); + assert!(parse_suite(r#"t"{class}""#).is_err()); + } + + #[test] + fn test_parse_tstring_not_equals() { + let source = r#"t"{1 != 2}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_equals() { + let source = r#"t"{42 == 42}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_self_doc_prec_space() { + let source = r#"t"{x =}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_self_doc_trailing_space() { + let source = r#"t"{x= }""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_yield_expr() { + let source = r#"t"{yield}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + #[test] fn test_parse_string_concat() { let source = "'Hello ' 'world'"; @@ -679,6 +841,62 @@ mod tests { insta::assert_debug_snapshot!(suite); } + #[test] + fn test_parse_t_string_concat_1() { + let source = "'Hello ' t'world'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_t_string_concat_2() { + let source = "'Hello ' t'world'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_t_string_concat_3() { + let source = "'Hello ' t'world{\"!\"}'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_t_string_concat_4() { + let source = "'Hello ' t'world{\"!\"}' 'again!'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_u_t_string_concat_1() { + let source = "u'Hello ' t'world'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_u_t_string_concat_2() { + let source = "u'Hello ' t'world' '!'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_f_t_string_concat_1() { + let source = "f'Hello ' t'world'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_f_t_string_concat_2() { + let source = "f'Hello ' t'world' '!'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + #[test] fn test_parse_string_triple_quotes_with_kind() { let source = "u'''Hello, world!'''"; @@ -796,6 +1014,71 @@ mod tests { insta::assert_debug_snapshot!(suite); } + #[test] + fn test_tstring_escaped_newline() { + let source = r#"t"\n{x}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_constant_range() { + let source = r#"t"aaa{bbb}ccc{ddd}eee""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_unescaped_newline() { + let source = r#"t""" +{x}""""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_escaped_character() { + let source = r#"t"\\{x}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_raw_tstring() { + let source = r#"rt"{x}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_triple_quoted_raw_tstring() { + let source = r#"rt"""{x}""""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_line_continuation() { + let source = r#"rt"\ +{x}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_nested_string_spec() { + let source = r#"t"{foo:{''}}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_nested_concatenation_string_spec() { + let source = r#"t"{foo:{'' ''}}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + /// #[test] fn test_dont_panic_on_8_in_octal_escape() { diff --git a/crates/ruff_python_parser/src/token.rs b/crates/ruff_python_parser/src/token.rs index 193aecac51..59e4c0581c 100644 --- a/crates/ruff_python_parser/src/token.rs +++ b/crates/ruff_python_parser/src/token.rs @@ -12,7 +12,7 @@ use bitflags::bitflags; use ruff_python_ast::name::Name; 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, BoolOp, Int, IpyEscapeKind, Operator, StringFlags, UnaryOp}; use ruff_text_size::{Ranged, TextRange}; @@ -48,7 +48,7 @@ impl Token { /// /// # Panics /// - /// If it isn't a string or any f-string tokens. + /// If it isn't a string or any f/t-string tokens. pub fn is_triple_quoted_string(self) -> bool { self.unwrap_string_flags().is_triple_quoted() } @@ -57,7 +57,7 @@ impl Token { /// /// # Panics /// - /// If it isn't a string or any f-string tokens. + /// If it isn't a string or any f/t-string tokens. pub fn string_quote_style(self) -> Quote { self.unwrap_string_flags().quote_style() } @@ -66,7 +66,7 @@ impl Token { /// /// # Panics /// - /// If it isn't a string or any f-string tokens. + /// If it isn't a string or any f/t-string tokens. pub fn unwrap_string_flags(self) -> AnyStringFlags { self.string_flags() .unwrap_or_else(|| panic!("token to be a string")) @@ -81,7 +81,8 @@ impl Token { } } - /// Returns `true` if this is any kind of string token. + /// Returns `true` if this is any kind of string token - including + /// tokens in t-strings (which do not have type `str`). const fn is_any_string(self) -> bool { matches!( self.kind, @@ -89,6 +90,9 @@ impl Token { | TokenKind::FStringStart | TokenKind::FStringMiddle | TokenKind::FStringEnd + | TokenKind::TStringStart + | TokenKind::TStringMiddle + | TokenKind::TStringEnd ) } } @@ -140,6 +144,14 @@ pub enum TokenKind { FStringMiddle, /// Token kind for the end of an f-string. This includes the closing quote. FStringEnd, + /// Token kind for the start of a t-string. This includes the `t`/`T`/`tr` prefix + /// and the opening quote(s). + TStringStart, + /// Token kind that includes the portion of text inside the t-string that's not + /// part of the interpolation part and isn't an opening or closing brace. + TStringMiddle, + /// Token kind for the end of a t-string. This includes the closing quote. + TStringEnd, /// Token kind for a IPython escape command. IpyEscapeCommand, /// Token kind for a comment. These are filtered out of the token stream prior to parsing. @@ -462,6 +474,11 @@ impl TokenKind { matches!(self, TokenKind::Plus | TokenKind::Minus) } + #[inline] + pub const fn is_interpolated_string_end(self) -> bool { + matches!(self, TokenKind::FStringEnd | TokenKind::TStringEnd) + } + /// Returns the [`UnaryOp`] that corresponds to this token kind, if it is a unary arithmetic /// operator, otherwise return [None]. /// @@ -613,6 +630,9 @@ impl fmt::Display for TokenKind { TokenKind::FStringStart => "FStringStart", TokenKind::FStringMiddle => "FStringMiddle", TokenKind::FStringEnd => "FStringEnd", + TokenKind::TStringStart => "TStringStart", + TokenKind::TStringMiddle => "TStringMiddle", + TokenKind::TStringEnd => "TStringEnd", TokenKind::IpyEscapeCommand => "IPython escape command", TokenKind::Comment => "comment", TokenKind::Question => "'?'", @@ -722,10 +742,12 @@ bitflags! { const BYTE_STRING = 1 << 3; /// The token is an f-string i.e., prefixed with `f` or `F` const F_STRING = 1 << 4; + /// The token is a t-string i.e., prefixed with `t` or `T` + const T_STRING = 1 << 5; /// The token is a raw string and the prefix character is in lowercase. - const RAW_STRING_LOWERCASE = 1 << 5; + const RAW_STRING_LOWERCASE = 1 << 6; /// The token is a raw string and the prefix character is in uppercase. - const RAW_STRING_UPPERCASE = 1 << 6; + const RAW_STRING_UPPERCASE = 1 << 7; /// The token is a raw string i.e., prefixed with `r` or `R` const RAW_STRING = Self::RAW_STRING_LOWERCASE.bits() | Self::RAW_STRING_UPPERCASE.bits(); @@ -758,6 +780,14 @@ impl StringFlags for TokenFlags { } else { AnyStringPrefix::Format(FStringPrefix::Regular) } + } else if self.intersects(TokenFlags::T_STRING) { + if self.intersects(TokenFlags::RAW_STRING_LOWERCASE) { + AnyStringPrefix::Template(TStringPrefix::Raw { uppercase_r: false }) + } else if self.intersects(TokenFlags::RAW_STRING_UPPERCASE) { + AnyStringPrefix::Template(TStringPrefix::Raw { uppercase_r: true }) + } else { + AnyStringPrefix::Template(TStringPrefix::Regular) + } } else if self.intersects(TokenFlags::BYTE_STRING) { if self.intersects(TokenFlags::RAW_STRING_LOWERCASE) { AnyStringPrefix::Bytes(ByteStringPrefix::Raw { uppercase_r: false }) @@ -784,9 +814,19 @@ impl TokenFlags { self.intersects(TokenFlags::F_STRING) } - /// Returns `true` if the token is a triple-quoted f-string. - pub(crate) fn is_triple_quoted_fstring(self) -> bool { - self.contains(TokenFlags::F_STRING | TokenFlags::TRIPLE_QUOTED_STRING) + /// Returns `true` if the token is a t-string. + pub(crate) const fn is_t_string(self) -> bool { + self.intersects(TokenFlags::T_STRING) + } + + /// Returns `true` if the token is a t-string. + pub(crate) const fn is_interpolated_string(self) -> bool { + self.intersects(TokenFlags::T_STRING.union(TokenFlags::F_STRING)) + } + + /// Returns `true` if the token is a triple-quoted t-string. + pub(crate) fn is_triple_quoted_interpolated_string(self) -> bool { + self.intersects(TokenFlags::TRIPLE_QUOTED_STRING) && self.is_interpolated_string() } /// Returns `true` if the token is a raw string. @@ -819,7 +859,7 @@ pub(crate) enum TokenValue { String(Box), /// Token value that includes the portion of text inside the f-string that's not /// part of the expression part and isn't an opening or closing brace. - FStringMiddle(Box), + InterpolatedStringMiddle(Box), /// Token value for IPython escape commands. These are recognized by the lexer /// only when the mode is [`Mode::Ipython`]. IpyEscapeCommand { diff --git a/crates/ruff_python_parser/tests/fixtures.rs b/crates/ruff_python_parser/tests/fixtures.rs index 9284e312db..7aa7a6f2e2 100644 --- a/crates/ruff_python_parser/tests/fixtures.rs +++ b/crates/ruff_python_parser/tests/fixtures.rs @@ -40,7 +40,7 @@ fn inline_err() { fn test_valid_syntax(input_path: &Path) { let source = fs::read_to_string(input_path).expect("Expected test file to exist"); let options = extract_options(&source).unwrap_or_else(|| { - ParseOptions::from(Mode::Module).with_target_version(PythonVersion::latest()) + ParseOptions::from(Mode::Module).with_target_version(PythonVersion::latest_preview()) }); let parsed = parse_unchecked(&source, options.clone()); @@ -133,7 +133,7 @@ fn test_valid_syntax(input_path: &Path) { fn test_invalid_syntax(input_path: &Path) { let source = fs::read_to_string(input_path).expect("Expected test file to exist"); let options = extract_options(&source).unwrap_or_else(|| { - ParseOptions::from(Mode::Module).with_target_version(PythonVersion::latest()) + ParseOptions::from(Mode::Module).with_target_version(PythonVersion::PY314) }); let parsed = parse_unchecked(&source, options.clone()); diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_annotation.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_annotation.py.snap index 76f99c4e88..b758b7438b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_annotation.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_annotation.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/ann_assign_stmt_invalid_annotation.py -snapshot_kind: text --- ## AST @@ -199,3 +198,23 @@ Module( 4 | x: y := int = 1 | ^^ Syntax Error: Expected a statement | + + +## Semantic Syntax Errors + + | +1 | x: *int = 1 +2 | x: yield a = 1 + | ^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation +3 | x: yield from b = 1 +4 | x: y := int = 1 + | + + + | +1 | x: *int = 1 +2 | x: yield a = 1 +3 | x: yield from b = 1 + | ^^^^^^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation +4 | x: y := int = 1 + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_empty_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_empty_expression.py.snap index 76a48f2197..c8b4ec3c12 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_empty_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_empty_expression.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/f_string_empty_expression.py -snapshot_kind: text --- ## AST @@ -22,8 +21,8 @@ Module( FString { range: 0..5, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..4, expression: Name( ExprName { @@ -63,8 +62,8 @@ Module( FString { range: 6..13, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 8..12, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_name_tok.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_name_tok.py.snap index 4a635ad345..36b66627ad 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_name_tok.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_name_tok.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/f_string_invalid_conversion_flag_name_tok.py -snapshot_kind: text --- ## AST @@ -22,8 +21,8 @@ Module( FString { range: 0..8, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..7, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_other_tok.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_other_tok.py.snap index 2f36142537..0ee7e5fe73 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_other_tok.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_other_tok.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/f_string_invalid_conversion_flag_other_tok.py -snapshot_kind: text --- ## AST @@ -22,8 +21,8 @@ Module( FString { range: 0..10, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..9, expression: Name( ExprName { @@ -63,8 +62,8 @@ Module( FString { range: 11..21, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 13..20, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_starred_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_starred_expr.py.snap index e02a5c22b8..f47c692d58 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_starred_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_starred_expr.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/f_string_invalid_starred_expr.py -snapshot_kind: text --- ## AST @@ -22,8 +21,8 @@ Module( FString { range: 77..83, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 79..82, expression: Starred( ExprStarred { @@ -69,8 +68,8 @@ Module( FString { range: 84..97, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 86..96, expression: Starred( ExprStarred { @@ -131,8 +130,8 @@ Module( FString { range: 98..111, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 100..110, expression: Starred( ExprStarred { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_lambda_without_parentheses.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_lambda_without_parentheses.py.snap index 05f1e2e3ff..121ca20d3c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_lambda_without_parentheses.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_lambda_without_parentheses.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/f_string_lambda_without_parentheses.py -snapshot_kind: text --- ## AST @@ -22,8 +21,8 @@ Module( FString { range: 0..16, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..12, expression: Lambda( ExprLambda { @@ -66,7 +65,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 12..14, value: " x", }, @@ -111,5 +110,5 @@ Module( | 1 | f"{lambda x: x}" - | ^ Syntax Error: Expected an f-string element or the end of the f-string + | ^ Syntax Error: Expected an f-string or t-string element or the end of the f-string or t-string | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace.py.snap index 7714d9e162..9fd2f1b9b2 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace.py.snap @@ -21,8 +21,8 @@ Module( FString { range: 0..4, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..3, expression: Name( ExprName { @@ -62,8 +62,8 @@ Module( FString { range: 5..14, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 7..14, expression: Name( ExprName { @@ -103,8 +103,8 @@ Module( FString { range: 15..23, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 17..22, expression: Name( ExprName { @@ -150,8 +150,8 @@ Module( FString { range: 24..28, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 26..27, expression: Name( ExprName { @@ -177,8 +177,8 @@ Module( FString { range: 29..37, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 33..34, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace_in_format_spec.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace_in_format_spec.py.snap index c72b2029e9..81f2c236a9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace_in_format_spec.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace_in_format_spec.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/f_string_unclosed_lbrace_in_format_spec.py -snapshot_kind: text --- ## AST @@ -23,13 +22,13 @@ Module( range: 0..12, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 2..8, value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 8..11, expression: Name( ExprName { @@ -41,7 +40,7 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 11..11, elements: [], }, @@ -75,13 +74,13 @@ Module( range: 13..28, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 15..21, value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 21..27, expression: Name( ExprName { @@ -93,11 +92,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 24..27, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 24..27, value: ".3f", }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_invalid_return_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_invalid_return_expr.py.snap index 0d1731ead8..f1968dedf9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_invalid_return_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_invalid_return_expr.py.snap @@ -179,3 +179,13 @@ Module( 3 | def foo() -> yield x: ... | ^^^^^^^ Syntax Error: Yield expression cannot be used here | + + +## Semantic Syntax Errors + + | +1 | def foo() -> *int: ... +2 | def foo() -> (*int): ... +3 | def foo() -> yield x: ... + | ^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@implicitly_concatenated_unterminated_string.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@implicitly_concatenated_unterminated_string.py.snap index 2ba54bef2f..5488502486 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@implicitly_concatenated_unterminated_string.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@implicitly_concatenated_unterminated_string.py.snap @@ -84,13 +84,13 @@ Module( range: 29..40, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 31..37, value: "world ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 37..40, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@implicitly_concatenated_unterminated_string_multiline.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@implicitly_concatenated_unterminated_string_multiline.py.snap index cdfccb4442..672216d159 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@implicitly_concatenated_unterminated_string_multiline.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@implicitly_concatenated_unterminated_string_multiline.py.snap @@ -34,13 +34,13 @@ Module( range: 18..31, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 20..26, value: "world ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 26..29, expression: Name( ExprName { @@ -132,7 +132,7 @@ Module( range: 68..76, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 70..75, value: "third", }, @@ -198,7 +198,7 @@ Module( 2 | 'hello' 3 | f'world {x} 4 | ) - | ^ Syntax Error: Expected an f-string element or the end of the f-string + | ^ Syntax Error: Expected an f-string or t-string element or the end of the f-string or t-string 5 | 1 + 1 6 | ( | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function.py.snap index b21adb9b16..66d0def077 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function.py.snap @@ -1397,7 +1397,7 @@ Module( | 1 | def d[T]() -> (await 1): ... - | ^^^^^^^ Syntax Error: await expression cannot be used within a generic definition + | ^^^^^^^ Syntax Error: await expression cannot be used within a type annotation 2 | def e[T](arg: (await 1)): ... 3 | def f[T]() -> (y := 3): ... | @@ -1406,7 +1406,7 @@ Module( | 1 | def d[T]() -> (await 1): ... 2 | def e[T](arg: (await 1)): ... - | ^^^^^^^ Syntax Error: await expression cannot be used within a generic definition + | ^^^^^^^ Syntax Error: await expression cannot be used within a type annotation 3 | def f[T]() -> (y := 3): ... 4 | def g[T](arg: (x := 1)): ... | @@ -1416,7 +1416,7 @@ Module( 1 | def d[T]() -> (await 1): ... 2 | def e[T](arg: (await 1)): ... 3 | def f[T]() -> (y := 3): ... - | ^^^^^^ Syntax Error: named expression cannot be used within a generic definition + | ^^^^^^ Syntax Error: named expression cannot be used within a type annotation 4 | def g[T](arg: (x := 1)): ... 5 | def h[T](x: (yield 1)): ... | @@ -1426,7 +1426,7 @@ Module( 2 | def e[T](arg: (await 1)): ... 3 | def f[T]() -> (y := 3): ... 4 | def g[T](arg: (x := 1)): ... - | ^^^^^^ Syntax Error: named expression cannot be used within a generic definition + | ^^^^^^ Syntax Error: named expression cannot be used within a type annotation 5 | def h[T](x: (yield 1)): ... 6 | def j[T]() -> (yield 1): ... | @@ -1436,7 +1436,7 @@ Module( 3 | def f[T]() -> (y := 3): ... 4 | def g[T](arg: (x := 1)): ... 5 | def h[T](x: (yield 1)): ... - | ^^^^^^^ Syntax Error: yield expression cannot be used within a generic definition + | ^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation 6 | def j[T]() -> (yield 1): ... 7 | def l[T](x: (yield from 1)): ... | @@ -1446,7 +1446,7 @@ Module( 4 | def g[T](arg: (x := 1)): ... 5 | def h[T](x: (yield 1)): ... 6 | def j[T]() -> (yield 1): ... - | ^^^^^^^ Syntax Error: yield expression cannot be used within a generic definition + | ^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation 7 | def l[T](x: (yield from 1)): ... 8 | def n[T]() -> (yield from 1): ... | @@ -1456,7 +1456,7 @@ Module( 5 | def h[T](x: (yield 1)): ... 6 | def j[T]() -> (yield 1): ... 7 | def l[T](x: (yield from 1)): ... - | ^^^^^^^^^^^^ Syntax Error: yield expression cannot be used within a generic definition + | ^^^^^^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation 8 | def n[T]() -> (yield from 1): ... 9 | def p[T: (yield 1)](): ... # yield in TypeVar bound | @@ -1466,7 +1466,7 @@ Module( 6 | def j[T]() -> (yield 1): ... 7 | def l[T](x: (yield from 1)): ... 8 | def n[T]() -> (yield from 1): ... - | ^^^^^^^^^^^^ Syntax Error: yield expression cannot be used within a generic definition + | ^^^^^^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation 9 | def p[T: (yield 1)](): ... # yield in TypeVar bound 10 | def q[T = (yield 1)](): ... # yield in TypeVar default | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_fstring_literal_element.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_fstring_literal_element.py.snap index 790a2e91ca..9c628a81d1 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_fstring_literal_element.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_fstring_literal_element.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/invalid_fstring_literal_element.py -snapshot_kind: text --- ## AST @@ -23,7 +22,7 @@ Module( range: 0..26, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 2..25, value: "", }, @@ -55,7 +54,7 @@ Module( range: 27..57, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 31..54, value: "", }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@mixed_bytes_and_non_bytes_literals.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@mixed_bytes_and_non_bytes_literals.py.snap index 07d4518fcc..35384a51a4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@mixed_bytes_and_non_bytes_literals.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@mixed_bytes_and_non_bytes_literals.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/mixed_bytes_and_non_bytes_literals.py -snapshot_kind: text --- ## AST @@ -61,7 +60,7 @@ Module( range: 18..26, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 20..25, value: "first", }, @@ -117,7 +116,7 @@ Module( range: 45..54, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 47..53, value: "second", }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_annotation.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_annotation.py.snap index c63c0fdb25..d799a78939 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_annotation.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_annotation.py.snap @@ -224,3 +224,13 @@ Module( 3 | def foo(arg: x := int): ... | ^^ Syntax Error: Expected ',', found ':=' | + + +## Semantic Syntax Errors + + | +1 | def foo(arg: *int): ... +2 | def foo(arg: yield int): ... + | ^^^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation +3 | def foo(arg: x := int): ... + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_star_annotation.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_star_annotation.py.snap index f036adb4b7..13eeeed689 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_star_annotation.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_star_annotation.py.snap @@ -308,3 +308,14 @@ Module( | ^^^^^^^ Syntax Error: Yield expression cannot be used here 5 | # def foo(*args: **int): ... | + + +## Semantic Syntax Errors + + | +2 | def foo(*args: (*tuple[int])): ... +3 | def foo(*args: *int or str): ... +4 | def foo(*args: *yield x): ... + | ^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation +5 | # def foo(*args: **int): ... + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep701_f_string_py311.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep701_f_string_py311.py.snap index 5ac816ecc8..34308fc597 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep701_f_string_py311.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep701_f_string_py311.py.snap @@ -22,13 +22,13 @@ Module( range: 44..74, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 46..58, value: "Magic wand: ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 58..73, expression: Subscript( ExprSubscript { @@ -92,8 +92,8 @@ Module( FString { range: 95..112, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 97..111, expression: Call( ExprCall { @@ -173,13 +173,13 @@ Module( range: 148..220, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 152..169, value: "A complex trick: ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 169..217, expression: Subscript( ExprSubscript { @@ -243,8 +243,8 @@ Module( FString { range: 221..254, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 223..253, expression: FString( ExprFString { @@ -255,8 +255,8 @@ Module( FString { range: 224..252, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 226..251, expression: FString( ExprFString { @@ -267,8 +267,8 @@ Module( FString { range: 227..250, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 229..249, expression: FString( ExprFString { @@ -279,8 +279,8 @@ Module( FString { range: 230..248, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 232..247, expression: FString( ExprFString { @@ -291,8 +291,8 @@ Module( FString { range: 233..246, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 235..245, expression: FString( ExprFString { @@ -303,8 +303,8 @@ Module( FString { range: 236..244, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 238..243, expression: BinOp( ExprBinOp { @@ -444,8 +444,8 @@ Module( FString { range: 276..310, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 278..303, expression: FString( ExprFString { @@ -456,8 +456,8 @@ Module( FString { range: 279..302, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 283..293, expression: StringLiteral( ExprStringLiteral { @@ -483,7 +483,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 293..299, value: " inner", }, @@ -506,7 +506,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 303..309, value: " outer", }, @@ -538,13 +538,13 @@ Module( range: 336..359, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 338..343, value: "test ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 343..353, expression: Name( ExprName { @@ -559,7 +559,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 353..358, value: " more", }, @@ -590,8 +590,8 @@ Module( FString { range: 403..422, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 407..419, expression: FString( ExprFString { @@ -602,8 +602,8 @@ Module( FString { range: 408..418, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 412..415, expression: Name( ExprName { @@ -660,8 +660,8 @@ Module( FString { range: 468..502, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 470..501, expression: Call( ExprCall { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token.py.snap index 48dc7f7866..9db9481b22 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token.py.snap @@ -527,13 +527,13 @@ Module( range: 895..905, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 897..903, value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 903..905, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap index dbc237b1d3..bb98cde311 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap @@ -22,13 +22,13 @@ Module( range: 162..192, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 164..171, value: "middle ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 171..191, expression: StringLiteral( ExprStringLiteral { @@ -51,11 +51,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 181..191, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 181..191, value: " ", }, @@ -116,13 +116,13 @@ Module( range: 207..228, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 209..216, value: "middle ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 216..228, expression: StringLiteral( ExprStringLiteral { @@ -145,11 +145,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 226..228, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 226..228, value: "\\", }, @@ -209,13 +209,13 @@ Module( range: 253..285, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 255..262, value: "middle ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 262..284, expression: StringLiteral( ExprStringLiteral { @@ -238,11 +238,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 272..284, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 272..284, value: "\\ ", }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_1.py.snap index 472a8579cc..efb2fecf82 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_1.py.snap @@ -22,13 +22,13 @@ Module( range: 166..178, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 170..176, value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 176..178, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_2.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_2.py.snap index ccf98eb430..c8431a27c9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_2.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/re_lexing/triple_quoted_fstring_2.py -snapshot_kind: text --- ## AST @@ -22,8 +21,8 @@ Module( FString { range: 167..183, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 171..180, expression: Name( ExprName { @@ -35,11 +34,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 176..180, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 176..180, value: ".3f\n", }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_3.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_3.py.snap index 4e4ba407ee..e59c846c0f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_3.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_3.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/re_lexing/triple_quoted_fstring_3.py -snapshot_kind: text --- ## AST @@ -35,8 +34,8 @@ Module( FString { range: 239..253, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 243..250, expression: Name( ExprName { @@ -48,11 +47,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 246..250, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 246..250, value: ".3f\n", }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_assignment_targets.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_assignment_targets.py.snap index 153ed4c251..7159d351f2 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_assignment_targets.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_assignment_targets.py.snap @@ -782,8 +782,8 @@ Module( FString { range: 576..585, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 578..584, expression: Name( ExprName { @@ -833,8 +833,8 @@ Module( FString { range: 591..609, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 593..598, expression: Name( ExprName { @@ -849,13 +849,13 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 598..603, value: " and ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 603..608, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_augmented_assignment_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_augmented_assignment_target.py.snap index 4fa30fed7b..ec4380012b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_augmented_assignment_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_augmented_assignment_target.py.snap @@ -688,8 +688,8 @@ Module( FString { range: 387..396, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 389..395, expression: Name( ExprName { @@ -738,8 +738,8 @@ Module( FString { range: 403..421, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 405..410, expression: Name( ExprName { @@ -754,13 +754,13 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 410..415, value: " and ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 415..420, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_empty_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_empty_expression.py.snap new file mode 100644 index 0000000000..423ad64c79 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_empty_expression.py.snap @@ -0,0 +1,113 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/t_string_empty_expression.py +--- +## AST + +``` +Module( + ModModule { + range: 0..58, + body: [ + Expr( + StmtExpr { + range: 44..49, + value: TString( + ExprTString { + range: 44..49, + value: TStringValue { + inner: Single( + TString( + TString { + range: 44..49, + elements: [ + Interpolation( + InterpolatedElement { + range: 46..48, + expression: Name( + ExprName { + range: 47..47, + id: Name(""), + ctx: Invalid, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 50..57, + value: TString( + ExprTString { + range: 50..57, + value: TStringValue { + inner: Single( + TString( + TString { + range: 50..57, + elements: [ + Interpolation( + InterpolatedElement { + range: 52..56, + expression: Name( + ExprName { + range: 53..53, + id: Name(""), + ctx: Invalid, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{}" + | ^ Syntax Error: Expected an expression +3 | t"{ }" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{}" +3 | t"{ }" + | ^ Syntax Error: Expected an expression + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_name_tok.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_name_tok.py.snap new file mode 100644 index 0000000000..b65770e8d8 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_name_tok.py.snap @@ -0,0 +1,63 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/t_string_invalid_conversion_flag_name_tok.py +--- +## AST + +``` +Module( + ModModule { + range: 0..53, + body: [ + Expr( + StmtExpr { + range: 44..52, + value: TString( + ExprTString { + range: 44..52, + value: TStringValue { + inner: Single( + TString( + TString { + range: 44..52, + elements: [ + Interpolation( + InterpolatedElement { + range: 46..51, + expression: Name( + ExprName { + range: 47..48, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{x!z}" + | ^ Syntax Error: t-string: invalid conversion character + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_other_tok.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_other_tok.py.snap new file mode 100644 index 0000000000..10324a349f --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_other_tok.py.snap @@ -0,0 +1,113 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/t_string_invalid_conversion_flag_other_tok.py +--- +## AST + +``` +Module( + ModModule { + range: 0..66, + body: [ + Expr( + StmtExpr { + range: 44..54, + value: TString( + ExprTString { + range: 44..54, + value: TStringValue { + inner: Single( + TString( + TString { + range: 44..54, + elements: [ + Interpolation( + InterpolatedElement { + range: 46..53, + expression: Name( + ExprName { + range: 47..48, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 55..65, + value: TString( + ExprTString { + range: 55..65, + value: TStringValue { + inner: Single( + TString( + TString { + range: 55..65, + elements: [ + Interpolation( + InterpolatedElement { + range: 57..64, + expression: Name( + ExprName { + range: 58..59, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{x!123}" + | ^^^ Syntax Error: t-string: invalid conversion character +3 | t"{x!'a'}" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{x!123}" +3 | t"{x!'a'}" + | ^^^ Syntax Error: t-string: invalid conversion character + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_starred_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_starred_expr.py.snap new file mode 100644 index 0000000000..a2fb0fda2d --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_starred_expr.py.snap @@ -0,0 +1,205 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/t_string_invalid_starred_expr.py +--- +## AST + +``` +Module( + ModModule { + range: 0..156, + body: [ + Expr( + StmtExpr { + range: 121..127, + value: TString( + ExprTString { + range: 121..127, + value: TStringValue { + inner: Single( + TString( + TString { + range: 121..127, + elements: [ + Interpolation( + InterpolatedElement { + range: 123..126, + expression: Starred( + ExprStarred { + range: 124..125, + value: Name( + ExprName { + range: 125..125, + id: Name(""), + ctx: Invalid, + }, + ), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 128..141, + value: TString( + ExprTString { + range: 128..141, + value: TStringValue { + inner: Single( + TString( + TString { + range: 128..141, + elements: [ + Interpolation( + InterpolatedElement { + range: 130..140, + expression: Starred( + ExprStarred { + range: 131..139, + value: BoolOp( + ExprBoolOp { + range: 132..139, + op: And, + values: [ + Name( + ExprName { + range: 132..133, + id: Name("x"), + ctx: Load, + }, + ), + Name( + ExprName { + range: 138..139, + id: Name("y"), + ctx: Load, + }, + ), + ], + }, + ), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 142..155, + value: TString( + ExprTString { + range: 142..155, + value: TStringValue { + inner: Single( + TString( + TString { + range: 142..155, + elements: [ + Interpolation( + InterpolatedElement { + range: 144..154, + expression: Starred( + ExprStarred { + range: 145..153, + value: Yield( + ExprYield { + range: 146..153, + value: Some( + Name( + ExprName { + range: 152..153, + id: Name("x"), + ctx: Load, + }, + ), + ), + }, + ), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.14"} +2 | # Starred expression inside t-string has a minimum precedence of bitwise or. +3 | t"{*}" + | ^ Syntax Error: Expected an expression +4 | t"{*x and y}" +5 | t"{*yield x}" + | + + + | +2 | # Starred expression inside t-string has a minimum precedence of bitwise or. +3 | t"{*}" +4 | t"{*x and y}" + | ^^^^^^^ Syntax Error: Boolean expression cannot be used here +5 | t"{*yield x}" + | + + + | +3 | t"{*}" +4 | t"{*x and y}" +5 | t"{*yield x}" + | ^^^^^^^ Syntax Error: Yield expression cannot be used here + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_lambda_without_parentheses.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_lambda_without_parentheses.py.snap new file mode 100644 index 0000000000..1138fe8530 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_lambda_without_parentheses.py.snap @@ -0,0 +1,118 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/t_string_lambda_without_parentheses.py +--- +## AST + +``` +Module( + ModModule { + range: 0..61, + body: [ + Expr( + StmtExpr { + range: 44..60, + value: TString( + ExprTString { + range: 44..60, + value: TStringValue { + inner: Single( + TString( + TString { + range: 44..60, + elements: [ + Interpolation( + InterpolatedElement { + range: 46..56, + expression: Lambda( + ExprLambda { + range: 47..56, + parameters: Some( + Parameters { + range: 54..55, + posonlyargs: [], + args: [ + ParameterWithDefault { + range: 54..55, + parameter: Parameter { + range: 54..55, + name: Identifier { + id: Name("x"), + range: 54..55, + }, + annotation: None, + }, + default: None, + }, + ], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + ), + body: Name( + ExprName { + range: 56..56, + id: Name(""), + ctx: Invalid, + }, + ), + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 56..58, + value: " x", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{lambda x: x}" + | ^^ Syntax Error: Expected an expression + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{lambda x: x}" + | ^^^^^^^^^ Syntax Error: t-string: lambda expressions are not allowed without parentheses + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{lambda x: x}" + | ^^ Syntax Error: t-string: expecting '}' + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{lambda x: x}" + | ^ Syntax Error: Expected an f-string or t-string element or the end of the f-string or t-string + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace.py.snap new file mode 100644 index 0000000000..b910137829 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace.py.snap @@ -0,0 +1,359 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/t_string_unclosed_lbrace.py +--- +## AST + +``` +Module( + ModModule { + range: 0..82, + body: [ + Expr( + StmtExpr { + range: 44..48, + value: TString( + ExprTString { + range: 44..48, + value: TStringValue { + inner: Single( + TString( + TString { + range: 44..48, + elements: [ + Interpolation( + InterpolatedElement { + range: 46..47, + expression: Name( + ExprName { + range: 47..47, + id: Name(""), + ctx: Invalid, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 49..58, + value: TString( + ExprTString { + range: 49..58, + value: TStringValue { + inner: Single( + TString( + TString { + range: 49..58, + elements: [ + Interpolation( + InterpolatedElement { + range: 51..58, + expression: Name( + ExprName { + range: 52..55, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 59..67, + value: TString( + ExprTString { + range: 59..67, + value: TStringValue { + inner: Single( + TString( + TString { + range: 59..67, + elements: [ + Interpolation( + InterpolatedElement { + range: 61..66, + expression: Name( + ExprName { + range: 62..65, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 68..81, + value: TString( + ExprTString { + range: 68..81, + value: TStringValue { + inner: Concatenated( + [ + TString( + TString { + range: 68..72, + elements: [ + Interpolation( + InterpolatedElement { + range: 70..71, + expression: Name( + ExprName { + range: 71..71, + id: Name(""), + ctx: Invalid, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 73..81, + elements: [ + Interpolation( + InterpolatedElement { + range: 77..78, + expression: Name( + ExprName { + range: 78..78, + id: Name(""), + ctx: Invalid, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{" + | ^ Syntax Error: missing closing quote in string literal +3 | t"{foo!r" +4 | t"{foo=" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{" + | ^ Syntax Error: t-string: unterminated string +3 | t"{foo!r" +4 | t"{foo=" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{" + | ^ Syntax Error: t-string: unterminated string +3 | t"{foo!r" +4 | t"{foo=" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{" +3 | t"{foo!r" + | ^^ Syntax Error: missing closing quote in string literal +4 | t"{foo=" +5 | t"{" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{" +3 | t"{foo!r" + | ^ Syntax Error: t-string: unterminated string +4 | t"{foo=" +5 | t"{" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{" +3 | t"{foo!r" + | ^ Syntax Error: t-string: unterminated string +4 | t"{foo=" +5 | t"{" + | + + + | +2 | t"{" +3 | t"{foo!r" +4 | t"{foo=" + | ^^ Syntax Error: t-string: expecting '}' +5 | t"{" +6 | t"""{""" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{" +3 | t"{foo!r" + | ^ Syntax Error: Expected TStringEnd, found Unknown +4 | t"{foo=" +5 | t"{" + | + + + | +2 | t"{" +3 | t"{foo!r" +4 | t"{foo=" + | ^ Syntax Error: missing closing quote in string literal +5 | t"{" +6 | t"""{""" + | + + + | +2 | t"{" +3 | t"{foo!r" +4 | t"{foo=" + | ^ Syntax Error: t-string: unterminated string +5 | t"{" +6 | t"""{""" + | + + + | +2 | t"{" +3 | t"{foo!r" +4 | t"{foo=" + | ^ Syntax Error: t-string: unterminated string +5 | t"{" +6 | t"""{""" + | + + + | +3 | t"{foo!r" +4 | t"{foo=" +5 | t"{" + | ^ Syntax Error: missing closing quote in string literal +6 | t"""{""" + | + + + | +4 | t"{foo=" +5 | t"{" +6 | t"""{""" + | ^^^^ Syntax Error: Expected TStringEnd, found TStringStart + | + + + | +4 | t"{foo=" +5 | t"{" +6 | t"""{""" + | ^^^ Syntax Error: Expected an expression + | + + + | +5 | t"{" +6 | t"""{""" + | ^ Syntax Error: unexpected EOF while parsing + | + + + | +5 | t"{" +6 | t"""{""" + | ^ Syntax Error: t-string: unterminated string + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace_in_format_spec.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace_in_format_spec.py.snap new file mode 100644 index 0000000000..3c16c4c161 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace_in_format_spec.py.snap @@ -0,0 +1,142 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/t_string_unclosed_lbrace_in_format_spec.py +--- +## AST + +``` +Module( + ModModule { + range: 0..73, + body: [ + Expr( + StmtExpr { + range: 44..56, + value: TString( + ExprTString { + range: 44..56, + value: TStringValue { + inner: Single( + TString( + TString { + range: 44..56, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 46..52, + value: "hello ", + }, + ), + Interpolation( + InterpolatedElement { + range: 52..55, + expression: Name( + ExprName { + range: 53..54, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 55..55, + elements: [], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 57..72, + value: TString( + ExprTString { + range: 57..72, + value: TStringValue { + inner: Single( + TString( + TString { + range: 57..72, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 59..65, + value: "hello ", + }, + ), + Interpolation( + InterpolatedElement { + range: 65..71, + expression: Name( + ExprName { + range: 66..67, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 68..71, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 68..71, + value: ".3f", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"hello {x:" + | ^ Syntax Error: t-string: expecting '}' +3 | t"hello {x:.3f" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"hello {x:" +3 | t"hello {x:.3f" + | ^ Syntax Error: t-string: expecting '}' + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unterminated_fstring_newline_recovery.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unterminated_fstring_newline_recovery.py.snap index c2c29483fd..ad595e25bb 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unterminated_fstring_newline_recovery.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unterminated_fstring_newline_recovery.py.snap @@ -74,13 +74,13 @@ Module( range: 14..24, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 16..22, value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 22..24, expression: Name( ExprName { @@ -148,13 +148,13 @@ Module( range: 31..42, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 33..39, value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 39..42, expression: Name( ExprName { @@ -166,7 +166,7 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 42..42, elements: [], }, @@ -227,13 +227,13 @@ Module( range: 49..60, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 51..57, value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 57..60, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@write_to_debug_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@write_to_debug_expr.py.snap index 617675786f..348aa344d8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@write_to_debug_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@write_to_debug_expr.py.snap @@ -173,7 +173,7 @@ Module( | 1 | del __debug__ - | ^^^^^^^^^ Syntax Error: cannot delete `__debug__` on Python 3.13 (syntax was removed in 3.9) + | ^^^^^^^^^ Syntax Error: cannot delete `__debug__` on Python 3.14 (syntax was removed in 3.9) 2 | del x, y, __debug__, z 3 | __debug__ = 1 | @@ -182,7 +182,7 @@ Module( | 1 | del __debug__ 2 | del x, y, __debug__, z - | ^^^^^^^^^ Syntax Error: cannot delete `__debug__` on Python 3.13 (syntax was removed in 3.9) + | ^^^^^^^^^ Syntax Error: cannot delete `__debug__` on Python 3.14 (syntax was removed in 3.9) 3 | __debug__ = 1 4 | x, y, __debug__, z = 1, 2, 3, 4 | diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__f_string.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__f_string.py.snap index 2fe8ea2855..99067deeda 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__f_string.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__f_string.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/f_string.py -snapshot_kind: text --- ## AST @@ -147,8 +146,8 @@ Module( FString { range: 47..56, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 49..55, expression: StringLiteral( ExprStringLiteral { @@ -199,8 +198,8 @@ Module( FString { range: 57..67, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 59..66, expression: Name( ExprName { @@ -240,8 +239,8 @@ Module( FString { range: 68..75, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 70..74, expression: Tuple( ExprTuple { @@ -291,8 +290,8 @@ Module( FString { range: 76..86, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 78..85, expression: Compare( ExprCompare { @@ -323,7 +322,7 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 84..84, elements: [], }, @@ -356,8 +355,8 @@ Module( FString { range: 87..102, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 89..101, expression: NumberLiteral( ExprNumberLiteral { @@ -370,11 +369,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 92..100, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 92..97, expression: StringLiteral( ExprStringLiteral { @@ -400,7 +399,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 97..100, value: ">10", }, @@ -436,8 +435,8 @@ Module( FString { range: 103..118, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 105..117, expression: NumberLiteral( ExprNumberLiteral { @@ -450,11 +449,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 108..116, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 108..113, expression: StringLiteral( ExprStringLiteral { @@ -480,7 +479,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 113..116, value: ">10", }, @@ -516,8 +515,8 @@ Module( FString { range: 119..133, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 121..132, expression: Name( ExprName { @@ -562,8 +561,8 @@ Module( FString { range: 134..154, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 136..153, expression: Name( ExprName { @@ -580,11 +579,11 @@ Module( ), conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 147..152, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 147..152, value: ".3f ", }, @@ -620,8 +619,8 @@ Module( FString { range: 155..173, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 157..172, expression: Name( ExprName { @@ -666,8 +665,8 @@ Module( FString { range: 174..190, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 176..189, expression: Tuple( ExprTuple { @@ -730,8 +729,8 @@ Module( FString { range: 191..217, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 193..216, expression: FString( ExprFString { @@ -742,8 +741,8 @@ Module( FString { range: 194..210, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 196..209, expression: NumberLiteral( ExprNumberLiteral { @@ -761,11 +760,11 @@ Module( ), conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 205..208, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 205..208, value: ".1f", }, @@ -790,11 +789,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 211..215, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 211..215, value: "*^20", }, @@ -849,13 +848,13 @@ Module( range: 227..242, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 229..233, value: "bar ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 233..240, expression: BinOp( ExprBinOp { @@ -883,7 +882,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 240..241, value: " ", }, @@ -1036,13 +1035,13 @@ Module( range: 347..364, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 349..350, value: "\\", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 350..355, expression: Name( ExprName { @@ -1057,13 +1056,13 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 355..356, value: "\\", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 356..363, expression: Name( ExprName { @@ -1075,11 +1074,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 361..362, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 361..362, value: "\\", }, @@ -1116,7 +1115,7 @@ Module( range: 365..379, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 367..378, value: "\\{foo\\}", }, @@ -1147,8 +1146,8 @@ Module( FString { range: 380..420, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 384..417, expression: Name( ExprName { @@ -1160,11 +1159,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 394..416, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 394..416, value: "x\n y\n z\n", }, @@ -1200,8 +1199,8 @@ Module( FString { range: 421..439, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 423..438, expression: Name( ExprName { @@ -1247,13 +1246,13 @@ Module( range: 441..486, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 443..450, value: "normal ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 450..455, expression: Name( ExprName { @@ -1268,13 +1267,13 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 455..468, value: " {another} ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 468..473, expression: Name( ExprName { @@ -1289,13 +1288,13 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 473..476, value: " {", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 476..483, expression: Name( ExprName { @@ -1310,7 +1309,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 483..485, value: "}", }, @@ -1342,13 +1341,13 @@ Module( range: 487..529, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 489..496, value: "normal ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 496..503, expression: Name( ExprName { @@ -1363,13 +1362,13 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 503..504, value: " ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 504..511, expression: Name( ExprName { @@ -1384,13 +1383,13 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 511..512, value: " ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 512..519, expression: Name( ExprName { @@ -1405,13 +1404,13 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 519..520, value: " ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 520..528, expression: Name( ExprName { @@ -1452,13 +1451,13 @@ Module( range: 530..549, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 532..539, value: "normal ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 539..548, expression: Name( ExprName { @@ -1470,11 +1469,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 542..547, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 542..547, value: "y + 2", }, @@ -1510,8 +1509,8 @@ Module( FString { range: 550..568, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 552..567, expression: Name( ExprName { @@ -1523,11 +1522,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 555..566, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 555..566, expression: Call( ExprCall { @@ -1600,8 +1599,8 @@ Module( FString { range: 569..588, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 571..587, expression: Lambda( ExprLambda { @@ -1676,8 +1675,8 @@ Module( FString { range: 589..597, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 591..596, expression: Name( ExprName { @@ -1722,8 +1721,8 @@ Module( FString { range: 598..611, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 600..610, expression: Name( ExprName { @@ -1768,8 +1767,8 @@ Module( FString { range: 612..621, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 614..620, expression: Name( ExprName { @@ -1814,8 +1813,8 @@ Module( FString { range: 622..636, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 624..635, expression: Name( ExprName { @@ -1827,11 +1826,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 627..634, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 627..634, value: ".3f!r =", }, @@ -1867,8 +1866,8 @@ Module( FString { range: 637..653, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 639..652, expression: Name( ExprName { @@ -1885,11 +1884,11 @@ Module( ), conversion: Repr, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 648..651, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 648..651, value: ".3f", }, @@ -1925,8 +1924,8 @@ Module( FString { range: 654..667, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 656..666, expression: Name( ExprName { @@ -1938,11 +1937,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 659..665, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 659..665, value: ".3f=!r", }, @@ -1990,8 +1989,8 @@ Module( FString { range: 676..682, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 678..681, expression: Name( ExprName { @@ -2033,8 +2032,8 @@ Module( FString { range: 683..689, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 685..688, expression: Name( ExprName { @@ -2060,8 +2059,8 @@ Module( FString { range: 690..696, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 692..695, expression: Name( ExprName { @@ -2103,8 +2102,8 @@ Module( FString { range: 697..703, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 699..702, expression: Name( ExprName { @@ -2157,13 +2156,13 @@ Module( range: 712..756, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 714..739, value: "Invalid args in command: ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 739..755, expression: Tuple( ExprTuple { @@ -2237,8 +2236,8 @@ Module( FString { range: 763..769, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 765..768, expression: Name( ExprName { @@ -2292,7 +2291,7 @@ Module( range: 782..786, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 784..785, value: "a", }, @@ -2310,7 +2309,7 @@ Module( range: 791..795, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 793..794, value: "b", }, @@ -2339,7 +2338,7 @@ Module( range: 808..813, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 811..812, value: "d", }, @@ -2359,7 +2358,7 @@ Module( range: 818..823, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 821..822, value: "e", }, @@ -2405,8 +2404,8 @@ Module( FString { range: 857..865, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 859..864, expression: Name( ExprName { @@ -2481,8 +2480,8 @@ Module( FString { range: 886..894, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 888..893, expression: Name( ExprName { @@ -2557,8 +2556,8 @@ Module( FString { range: 916..924, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 918..923, expression: Name( ExprName { @@ -2634,13 +2633,13 @@ Module( range: 947..966, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 949..953, value: "bar ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 953..958, expression: Name( ExprName { @@ -2655,7 +2654,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 958..965, value: " really", }, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__t_string.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__t_string.py.snap new file mode 100644 index 0000000000..99a7bcd3b9 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__t_string.py.snap @@ -0,0 +1,3127 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/valid/expressions/t_string.py +--- +## AST + +``` +Module( + ModModule { + range: 0..1143, + body: [ + Expr( + StmtExpr { + range: 18..21, + value: TString( + ExprTString { + range: 18..21, + value: TStringValue { + inner: Single( + TString( + TString { + range: 18..21, + elements: [], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 22..25, + value: TString( + ExprTString { + range: 22..25, + value: TStringValue { + inner: Single( + TString( + TString { + range: 22..25, + elements: [], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 26..29, + value: TString( + ExprTString { + range: 26..29, + value: TStringValue { + inner: Single( + TString( + TString { + range: 26..29, + elements: [], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 30..37, + value: TString( + ExprTString { + range: 30..37, + value: TStringValue { + inner: Single( + TString( + TString { + range: 30..37, + elements: [], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 38..45, + value: TString( + ExprTString { + range: 38..45, + value: TStringValue { + inner: Single( + TString( + TString { + range: 38..45, + elements: [], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 47..56, + value: TString( + ExprTString { + range: 47..56, + value: TStringValue { + inner: Single( + TString( + TString { + range: 47..56, + elements: [ + Interpolation( + InterpolatedElement { + range: 49..55, + expression: StringLiteral( + ExprStringLiteral { + range: 50..54, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 50..54, + value: " t", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 57..67, + value: TString( + ExprTString { + range: 57..67, + value: TStringValue { + inner: Single( + TString( + TString { + range: 57..67, + elements: [ + Interpolation( + InterpolatedElement { + range: 59..66, + expression: Name( + ExprName { + range: 60..63, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: Str, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 68..75, + value: TString( + ExprTString { + range: 68..75, + value: TStringValue { + inner: Single( + TString( + TString { + range: 68..75, + elements: [ + Interpolation( + InterpolatedElement { + range: 70..74, + expression: Tuple( + ExprTuple { + range: 71..73, + elts: [ + NumberLiteral( + ExprNumberLiteral { + range: 71..72, + value: Int( + 3, + ), + }, + ), + ], + ctx: Load, + parenthesized: false, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 76..86, + value: TString( + ExprTString { + range: 76..86, + value: TStringValue { + inner: Single( + TString( + TString { + range: 76..86, + elements: [ + Interpolation( + InterpolatedElement { + range: 78..85, + expression: Compare( + ExprCompare { + range: 79..83, + left: NumberLiteral( + ExprNumberLiteral { + range: 79..80, + value: Int( + 3, + ), + }, + ), + ops: [ + NotEq, + ], + comparators: [ + NumberLiteral( + ExprNumberLiteral { + range: 82..83, + value: Int( + 4, + ), + }, + ), + ], + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 84..84, + elements: [], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 87..102, + value: TString( + ExprTString { + range: 87..102, + value: TStringValue { + inner: Single( + TString( + TString { + range: 87..102, + elements: [ + Interpolation( + InterpolatedElement { + range: 89..101, + expression: NumberLiteral( + ExprNumberLiteral { + range: 90..91, + value: Int( + 3, + ), + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 92..100, + elements: [ + Interpolation( + InterpolatedElement { + range: 92..97, + expression: StringLiteral( + ExprStringLiteral { + range: 93..96, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 93..96, + value: "}", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 97..100, + value: ">10", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 103..118, + value: TString( + ExprTString { + range: 103..118, + value: TStringValue { + inner: Single( + TString( + TString { + range: 103..118, + elements: [ + Interpolation( + InterpolatedElement { + range: 105..117, + expression: NumberLiteral( + ExprNumberLiteral { + range: 106..107, + value: Int( + 3, + ), + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 108..116, + elements: [ + Interpolation( + InterpolatedElement { + range: 108..113, + expression: StringLiteral( + ExprStringLiteral { + range: 109..112, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 109..112, + value: "{", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 113..116, + value: ">10", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 119..133, + value: TString( + ExprTString { + range: 119..133, + value: TStringValue { + inner: Single( + TString( + TString { + range: 119..133, + elements: [ + Interpolation( + InterpolatedElement { + range: 121..132, + expression: Name( + ExprName { + range: 124..127, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: " ", + trailing: " = ", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 134..154, + value: TString( + ExprTString { + range: 134..154, + value: TStringValue { + inner: Single( + TString( + TString { + range: 134..154, + elements: [ + Interpolation( + InterpolatedElement { + range: 136..153, + expression: Name( + ExprName { + range: 139..142, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: " ", + trailing: " = ", + }, + ), + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 147..152, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 147..152, + value: ".3f ", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 155..173, + value: TString( + ExprTString { + range: 155..173, + value: TStringValue { + inner: Single( + TString( + TString { + range: 155..173, + elements: [ + Interpolation( + InterpolatedElement { + range: 157..172, + expression: Name( + ExprName { + range: 160..163, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: " ", + trailing: " = ", + }, + ), + conversion: Str, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 174..190, + value: TString( + ExprTString { + range: 174..190, + value: TStringValue { + inner: Single( + TString( + TString { + range: 174..190, + elements: [ + Interpolation( + InterpolatedElement { + range: 176..189, + expression: Tuple( + ExprTuple { + range: 179..183, + elts: [ + NumberLiteral( + ExprNumberLiteral { + range: 179..180, + value: Int( + 1, + ), + }, + ), + NumberLiteral( + ExprNumberLiteral { + range: 182..183, + value: Int( + 2, + ), + }, + ), + ], + ctx: Load, + parenthesized: false, + }, + ), + debug_text: Some( + DebugText { + leading: " ", + trailing: " = ", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 191..217, + value: TString( + ExprTString { + range: 191..217, + value: TStringValue { + inner: Single( + TString( + TString { + range: 191..217, + elements: [ + Interpolation( + InterpolatedElement { + range: 193..216, + expression: TString( + ExprTString { + range: 194..210, + value: TStringValue { + inner: Single( + TString( + TString { + range: 194..210, + elements: [ + Interpolation( + InterpolatedElement { + range: 196..209, + expression: NumberLiteral( + ExprNumberLiteral { + range: 197..203, + value: Float( + 3.1415, + ), + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 205..208, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 205..208, + value: ".1f", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 211..215, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 211..215, + value: "*^20", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 219..253, + value: Dict( + ExprDict { + range: 219..253, + items: [ + DictItem { + key: Some( + TString( + ExprTString { + range: 220..248, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 220..226, + value: "foo ", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 227..242, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 229..233, + value: "bar ", + }, + ), + Interpolation( + InterpolatedElement { + range: 233..240, + expression: BinOp( + ExprBinOp { + range: 234..239, + left: Name( + ExprName { + range: 234..235, + id: Name("x"), + ctx: Load, + }, + ), + op: Add, + right: Name( + ExprName { + range: 238..239, + id: Name("y"), + ctx: Load, + }, + ), + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 240..241, + value: " ", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 243..248, + value: "baz", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + ), + value: NumberLiteral( + ExprNumberLiteral { + range: 250..252, + value: Int( + 10, + ), + }, + ), + }, + ], + }, + ), + }, + ), + Match( + StmtMatch { + range: 254..345, + subject: Name( + ExprName { + range: 260..263, + id: Name("foo"), + ctx: Load, + }, + ), + cases: [ + MatchCase { + range: 269..293, + pattern: MatchValue( + PatternMatchValue { + range: 274..279, + value: StringLiteral( + ExprStringLiteral { + range: 274..279, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 274..279, + value: "one", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + }, + ), + guard: None, + body: [ + Pass( + StmtPass { + range: 289..293, + }, + ), + ], + }, + MatchCase { + range: 298..345, + pattern: MatchValue( + PatternMatchValue { + range: 303..331, + value: StringLiteral( + ExprStringLiteral { + range: 303..331, + value: StringLiteralValue { + inner: Concatenated( + ConcatenatedStringLiteral { + strings: [ + StringLiteral { + range: 303..316, + value: "implicitly ", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + StringLiteral { + range: 317..331, + value: "concatenated", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ], + value: "implicitly concatenated", + }, + ), + }, + }, + ), + }, + ), + guard: None, + body: [ + Pass( + StmtPass { + range: 341..345, + }, + ), + ], + }, + ], + }, + ), + Expr( + StmtExpr { + range: 347..364, + value: TString( + ExprTString { + range: 347..364, + value: TStringValue { + inner: Single( + TString( + TString { + range: 347..364, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 349..350, + value: "\\", + }, + ), + Interpolation( + InterpolatedElement { + range: 350..355, + expression: Name( + ExprName { + range: 351..354, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 355..356, + value: "\\", + }, + ), + Interpolation( + InterpolatedElement { + range: 356..363, + expression: Name( + ExprName { + range: 357..360, + id: Name("bar"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 361..362, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 361..362, + value: "\\", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 365..379, + value: TString( + ExprTString { + range: 365..379, + value: TStringValue { + inner: Single( + TString( + TString { + range: 365..379, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 367..378, + value: "\\{foo\\}", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 380..420, + value: TString( + ExprTString { + range: 380..420, + value: TStringValue { + inner: Single( + TString( + TString { + range: 380..420, + elements: [ + Interpolation( + InterpolatedElement { + range: 384..417, + expression: Name( + ExprName { + range: 390..393, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 394..416, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 394..416, + value: "x\n y\n z\n", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 421..439, + value: TString( + ExprTString { + range: 421..439, + value: TStringValue { + inner: Single( + TString( + TString { + range: 421..439, + elements: [ + Interpolation( + InterpolatedElement { + range: 423..438, + expression: Name( + ExprName { + range: 428..431, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: " ( ", + trailing: " ) = ", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 441..486, + value: TString( + ExprTString { + range: 441..486, + value: TStringValue { + inner: Single( + TString( + TString { + range: 441..486, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 443..450, + value: "normal ", + }, + ), + Interpolation( + InterpolatedElement { + range: 450..455, + expression: Name( + ExprName { + range: 451..454, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 455..468, + value: " {another} ", + }, + ), + Interpolation( + InterpolatedElement { + range: 468..473, + expression: Name( + ExprName { + range: 469..472, + id: Name("bar"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 473..476, + value: " {", + }, + ), + Interpolation( + InterpolatedElement { + range: 476..483, + expression: Name( + ExprName { + range: 477..482, + id: Name("three"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 483..485, + value: "}", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 487..529, + value: TString( + ExprTString { + range: 487..529, + value: TStringValue { + inner: Single( + TString( + TString { + range: 487..529, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 489..496, + value: "normal ", + }, + ), + Interpolation( + InterpolatedElement { + range: 496..503, + expression: Name( + ExprName { + range: 497..500, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: Ascii, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 503..504, + value: " ", + }, + ), + Interpolation( + InterpolatedElement { + range: 504..511, + expression: Name( + ExprName { + range: 505..508, + id: Name("bar"), + ctx: Load, + }, + ), + debug_text: None, + conversion: Str, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 511..512, + value: " ", + }, + ), + Interpolation( + InterpolatedElement { + range: 512..519, + expression: Name( + ExprName { + range: 513..516, + id: Name("baz"), + ctx: Load, + }, + ), + debug_text: None, + conversion: Repr, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 519..520, + value: " ", + }, + ), + Interpolation( + InterpolatedElement { + range: 520..528, + expression: Name( + ExprName { + range: 521..527, + id: Name("foobar"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 530..549, + value: TString( + ExprTString { + range: 530..549, + value: TStringValue { + inner: Single( + TString( + TString { + range: 530..549, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 532..539, + value: "normal ", + }, + ), + Interpolation( + InterpolatedElement { + range: 539..548, + expression: Name( + ExprName { + range: 540..541, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 542..547, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 542..547, + value: "y + 2", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 550..568, + value: TString( + ExprTString { + range: 550..568, + value: TStringValue { + inner: Single( + TString( + TString { + range: 550..568, + elements: [ + Interpolation( + InterpolatedElement { + range: 552..567, + expression: Name( + ExprName { + range: 553..554, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 555..566, + elements: [ + Interpolation( + InterpolatedElement { + range: 555..566, + expression: Call( + ExprCall { + range: 556..565, + func: Attribute( + ExprAttribute { + range: 556..563, + value: Set( + ExprSet { + range: 556..559, + elts: [ + NumberLiteral( + ExprNumberLiteral { + range: 557..558, + value: Int( + 1, + ), + }, + ), + ], + }, + ), + attr: Identifier { + id: Name("pop"), + range: 560..563, + }, + ctx: Load, + }, + ), + arguments: Arguments { + range: 563..565, + args: [], + keywords: [], + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 569..588, + value: TString( + ExprTString { + range: 569..588, + value: TStringValue { + inner: Single( + TString( + TString { + range: 569..588, + elements: [ + Interpolation( + InterpolatedElement { + range: 571..587, + expression: Lambda( + ExprLambda { + range: 573..585, + parameters: Some( + Parameters { + range: 580..581, + posonlyargs: [], + args: [ + ParameterWithDefault { + range: 580..581, + parameter: Parameter { + range: 580..581, + name: Identifier { + id: Name("x"), + range: 580..581, + }, + annotation: None, + }, + default: None, + }, + ], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + ), + body: Set( + ExprSet { + range: 582..585, + elts: [ + Name( + ExprName { + range: 583..584, + id: Name("x"), + ctx: Load, + }, + ), + ], + }, + ), + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 589..597, + value: TString( + ExprTString { + range: 589..597, + value: TStringValue { + inner: Single( + TString( + TString { + range: 589..597, + elements: [ + Interpolation( + InterpolatedElement { + range: 591..596, + expression: Name( + ExprName { + range: 592..593, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: " =", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 598..611, + value: TString( + ExprTString { + range: 598..611, + value: TStringValue { + inner: Single( + TString( + TString { + range: 598..611, + elements: [ + Interpolation( + InterpolatedElement { + range: 600..610, + expression: Name( + ExprName { + range: 605..606, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: " ", + trailing: " = ", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 612..621, + value: TString( + ExprTString { + range: 612..621, + value: TStringValue { + inner: Single( + TString( + TString { + range: 612..621, + elements: [ + Interpolation( + InterpolatedElement { + range: 614..620, + expression: Name( + ExprName { + range: 615..616, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: Ascii, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 622..636, + value: TString( + ExprTString { + range: 622..636, + value: TStringValue { + inner: Single( + TString( + TString { + range: 622..636, + elements: [ + Interpolation( + InterpolatedElement { + range: 624..635, + expression: Name( + ExprName { + range: 625..626, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 627..634, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 627..634, + value: ".3f!r =", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 637..653, + value: TString( + ExprTString { + range: 637..653, + value: TStringValue { + inner: Single( + TString( + TString { + range: 637..653, + elements: [ + Interpolation( + InterpolatedElement { + range: 639..652, + expression: Name( + ExprName { + range: 640..641, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: " = ", + }, + ), + conversion: Repr, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 648..651, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 648..651, + value: ".3f", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 654..667, + value: TString( + ExprTString { + range: 654..667, + value: TStringValue { + inner: Single( + TString( + TString { + range: 654..667, + elements: [ + Interpolation( + InterpolatedElement { + range: 656..666, + expression: Name( + ExprName { + range: 657..658, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 659..665, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 659..665, + value: ".3f=!r", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 668..682, + value: TString( + ExprTString { + range: 668..682, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 668..675, + value: "hello", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 676..682, + elements: [ + Interpolation( + InterpolatedElement { + range: 678..681, + expression: Name( + ExprName { + range: 679..680, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 683..696, + value: TString( + ExprTString { + range: 683..696, + value: TStringValue { + inner: Concatenated( + [ + TString( + TString { + range: 683..689, + elements: [ + Interpolation( + InterpolatedElement { + range: 685..688, + expression: Name( + ExprName { + range: 686..687, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 690..696, + elements: [ + Interpolation( + InterpolatedElement { + range: 692..695, + expression: Name( + ExprName { + range: 693..694, + id: Name("y"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 697..711, + value: TString( + ExprTString { + range: 697..711, + value: TStringValue { + inner: Concatenated( + [ + TString( + TString { + range: 697..703, + elements: [ + Interpolation( + InterpolatedElement { + range: 699..702, + expression: Name( + ExprName { + range: 700..701, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 704..711, + value: "world", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 712..756, + value: TString( + ExprTString { + range: 712..756, + value: TStringValue { + inner: Single( + TString( + TString { + range: 712..756, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 714..739, + value: "Invalid args in command: ", + }, + ), + Interpolation( + InterpolatedElement { + range: 739..755, + expression: Tuple( + ExprTuple { + range: 740..754, + elts: [ + Name( + ExprName { + range: 740..747, + id: Name("command"), + ctx: Load, + }, + ), + Starred( + ExprStarred { + range: 749..754, + value: Name( + ExprName { + range: 750..754, + id: Name("args"), + ctx: Load, + }, + ), + ctx: Load, + }, + ), + ], + ctx: Load, + parenthesized: false, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 757..775, + value: TString( + ExprTString { + range: 757..775, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 757..762, + value: "foo", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 763..769, + elements: [ + Interpolation( + InterpolatedElement { + range: 765..768, + expression: Name( + ExprName { + range: 766..767, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 770..775, + value: "bar", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 776..825, + value: TString( + ExprTString { + range: 782..823, + value: TStringValue { + inner: Concatenated( + [ + TString( + TString { + range: 782..786, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 784..785, + value: "a", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 791..795, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 793..794, + value: "b", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 800..803, + value: "c", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 808..813, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 811..812, + value: "d", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Raw { + uppercase_r: false, + }, + triple_quoted: false, + }, + }, + ), + FString( + FString { + range: 818..823, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 821..822, + value: "e", + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Raw { + uppercase_r: false, + }, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 850..879, + value: TString( + ExprTString { + range: 850..879, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 850..856, + value: "foo", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Unicode, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 857..865, + elements: [ + Interpolation( + InterpolatedElement { + range: 859..864, + expression: Name( + ExprName { + range: 860..863, + id: Name("bar"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 866..871, + value: "baz", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 872..879, + value: " some", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 880..909, + value: TString( + ExprTString { + range: 880..909, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 880..885, + value: "foo", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 886..894, + elements: [ + Interpolation( + InterpolatedElement { + range: 888..893, + expression: Name( + ExprName { + range: 889..892, + id: Name("bar"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 895..901, + value: "baz", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Unicode, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 902..909, + value: " some", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 910..939, + value: TString( + ExprTString { + range: 910..939, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 910..915, + value: "foo", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 916..924, + elements: [ + Interpolation( + InterpolatedElement { + range: 918..923, + expression: Name( + ExprName { + range: 919..922, + id: Name("bar"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 925..930, + value: "baz", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 931..939, + value: " some", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Unicode, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 940..978, + value: TString( + ExprTString { + range: 940..978, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 940..946, + value: "foo", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Unicode, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 947..966, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 949..953, + value: "bar ", + }, + ), + Interpolation( + InterpolatedElement { + range: 953..958, + expression: Name( + ExprName { + range: 954..957, + id: Name("baz"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 958..965, + value: " really", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 967..973, + value: "bar", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Unicode, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 974..978, + value: "no", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 998..1017, + value: TString( + ExprTString { + range: 998..1017, + value: TStringValue { + inner: Concatenated( + [ + FString( + FString { + range: 998..1007, + elements: [ + Interpolation( + InterpolatedElement { + range: 1000..1006, + expression: Name( + ExprName { + range: 1001..1005, + id: Name("this"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 1008..1017, + elements: [ + Interpolation( + InterpolatedElement { + range: 1010..1016, + expression: Name( + ExprName { + range: 1011..1015, + id: Name("that"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 1018..1036, + value: TString( + ExprTString { + range: 1018..1036, + value: TStringValue { + inner: Concatenated( + [ + TString( + TString { + range: 1018..1027, + elements: [ + Interpolation( + InterpolatedElement { + range: 1020..1026, + expression: Name( + ExprName { + range: 1021..1025, + id: Name("this"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + FString( + FString { + range: 1027..1036, + elements: [ + Interpolation( + InterpolatedElement { + range: 1029..1035, + expression: Name( + ExprName { + range: 1030..1034, + id: Name("that"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 1037..1064, + value: TString( + ExprTString { + range: 1037..1064, + value: TStringValue { + inner: Concatenated( + [ + TString( + TString { + range: 1037..1046, + elements: [ + Interpolation( + InterpolatedElement { + range: 1039..1045, + expression: Name( + ExprName { + range: 1040..1044, + id: Name("this"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 1047..1053, + value: "that", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + FString( + FString { + range: 1054..1064, + elements: [ + Interpolation( + InterpolatedElement { + range: 1056..1063, + expression: Name( + ExprName { + range: 1057..1062, + id: Name("other"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 1065..1111, + value: TString( + ExprTString { + range: 1065..1111, + value: TStringValue { + inner: Concatenated( + [ + FString( + FString { + range: 1065..1082, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 1067..1071, + value: "one ", + }, + ), + Interpolation( + InterpolatedElement { + range: 1071..1077, + expression: Name( + ExprName { + range: 1072..1076, + id: Name("this"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 1077..1081, + value: " two", + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 1083..1089, + value: "that", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 1090..1111, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 1092..1098, + value: "three ", + }, + ), + Interpolation( + InterpolatedElement { + range: 1098..1105, + expression: Name( + ExprName { + range: 1099..1104, + id: Name("other"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 1105..1110, + value: " four", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 1123..1142, + value: TString( + ExprTString { + range: 1123..1142, + value: TStringValue { + inner: Single( + TString( + TString { + range: 1123..1142, + elements: [ + Interpolation( + InterpolatedElement { + range: 1125..1141, + expression: FString( + ExprFString { + range: 1126..1140, + value: FStringValue { + inner: Single( + FString( + FString { + range: 1126..1140, + elements: [ + Interpolation( + InterpolatedElement { + range: 1128..1139, + expression: TString( + ExprTString { + range: 1129..1138, + value: TStringValue { + inner: Single( + TString( + TString { + range: 1129..1138, + elements: [ + Interpolation( + InterpolatedElement { + range: 1131..1137, + expression: Name( + ExprName { + range: 1132..1136, + id: Name("this"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@fstring_format_spec_terminator.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@fstring_format_spec_terminator.py.snap index 1e672a1ce1..34167d13a7 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@fstring_format_spec_terminator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@fstring_format_spec_terminator.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/fstring_format_spec_terminator.py -snapshot_kind: text --- ## AST @@ -23,13 +22,13 @@ Module( range: 0..19, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 2..8, value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 8..12, expression: Name( ExprName { @@ -41,7 +40,7 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 11..11, elements: [], }, @@ -49,7 +48,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 12..18, value: " world", }, @@ -81,13 +80,13 @@ Module( range: 20..42, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 22..28, value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 28..35, expression: Name( ExprName { @@ -99,11 +98,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 31..34, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 31..34, value: ".3f", }, @@ -114,7 +113,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 35..41, value: " world", }, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_1.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_1.py.snap index 044de1e157..16fc4e36e3 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_1.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/match_classify_as_keyword_1.py -snapshot_kind: text --- ## AST @@ -223,13 +222,13 @@ Module( range: 140..150, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 142..146, value: "foo ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 146..149, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_annotation.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_annotation.py.snap index c3d622ebb0..004ec6c603 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_annotation.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_annotation.py.snap @@ -7,7 +7,7 @@ input_file: crates/ruff_python_parser/resources/inline/ok/param_with_annotation. ``` Module( ModModule { - range: 0..84, + range: 0..54, body: [ FunctionDef( StmtFunctionDef { @@ -145,72 +145,6 @@ Module( ], }, ), - FunctionDef( - StmtFunctionDef { - range: 54..83, - is_async: false, - decorator_list: [], - name: Identifier { - id: Name("foo"), - range: 58..61, - }, - type_params: None, - parameters: Parameters { - range: 61..78, - posonlyargs: [], - args: [ - ParameterWithDefault { - range: 62..77, - parameter: Parameter { - range: 62..77, - name: Identifier { - id: Name("arg"), - range: 62..65, - }, - annotation: Some( - Named( - ExprNamed { - range: 68..76, - target: Name( - ExprName { - range: 68..69, - id: Name("x"), - ctx: Store, - }, - ), - value: Name( - ExprName { - range: 73..76, - id: Name("int"), - ctx: Load, - }, - ), - }, - ), - ), - }, - default: None, - }, - ], - vararg: None, - kwonlyargs: [], - kwarg: None, - }, - returns: None, - body: [ - Expr( - StmtExpr { - range: 80..83, - value: EllipsisLiteral( - ExprEllipsisLiteral { - range: 80..83, - }, - ), - }, - ), - ], - }, - ), ], }, ) diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py311.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py311.py.snap index d4dcb42151..cbec152ee7 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py311.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py311.py.snap @@ -22,13 +22,13 @@ Module( range: 44..72, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 46..52, value: "outer ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 52..71, expression: StringLiteral( ExprStringLiteral { @@ -80,13 +80,13 @@ Module( range: 73..106, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 75..81, value: "outer ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 81..105, expression: Name( ExprName { @@ -98,11 +98,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 84..104, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 84..103, expression: StringLiteral( ExprStringLiteral { @@ -128,7 +128,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 103..104, value: " ", }, @@ -164,8 +164,8 @@ Module( FString { range: 107..147, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 111..144, expression: FString( ExprFString { @@ -176,8 +176,8 @@ Module( FString { range: 112..143, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 116..140, expression: FString( ExprFString { @@ -188,8 +188,8 @@ Module( FString { range: 117..139, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 119..138, expression: StringLiteral( ExprStringLiteral { @@ -274,8 +274,8 @@ Module( FString { range: 148..230, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 152..208, expression: FString( ExprFString { @@ -287,13 +287,13 @@ Module( range: 153..207, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 157..177, value: "# before expression ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 177..204, expression: FString( ExprFString { @@ -305,13 +305,13 @@ Module( range: 178..203, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 180..185, value: "# aro", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 185..197, expression: FString( ExprFString { @@ -323,13 +323,13 @@ Module( range: 186..196, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 188..189, value: "#", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 189..194, expression: BinOp( ExprBinOp { @@ -359,7 +359,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 194..195, value: "#", }, @@ -382,7 +382,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 197..202, value: "und #", }, @@ -422,7 +422,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 208..227, value: " # after expression", }, @@ -454,13 +454,13 @@ Module( range: 231..263, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 233..254, value: "escape outside of \t ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 254..260, expression: Name( ExprName { @@ -475,7 +475,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 260..262, value: "\n", }, @@ -507,7 +507,7 @@ Module( range: 264..277, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 266..276, value: "test\"abcd", }, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py312.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py312.py.snap index c9eea80822..09817c8283 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py312.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py312.py.snap @@ -22,13 +22,13 @@ Module( range: 44..74, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 46..58, value: "Magic wand: ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 58..73, expression: Subscript( ExprSubscript { @@ -92,8 +92,8 @@ Module( FString { range: 95..112, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 97..111, expression: Call( ExprCall { @@ -173,13 +173,13 @@ Module( range: 148..220, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 152..169, value: "A complex trick: ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 169..217, expression: Subscript( ExprSubscript { @@ -243,8 +243,8 @@ Module( FString { range: 221..254, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 223..253, expression: FString( ExprFString { @@ -255,8 +255,8 @@ Module( FString { range: 224..252, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 226..251, expression: FString( ExprFString { @@ -267,8 +267,8 @@ Module( FString { range: 227..250, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 229..249, expression: FString( ExprFString { @@ -279,8 +279,8 @@ Module( FString { range: 230..248, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 232..247, expression: FString( ExprFString { @@ -291,8 +291,8 @@ Module( FString { range: 233..246, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 235..245, expression: FString( ExprFString { @@ -303,8 +303,8 @@ Module( FString { range: 236..244, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 238..243, expression: BinOp( ExprBinOp { @@ -444,8 +444,8 @@ Module( FString { range: 276..310, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 278..303, expression: FString( ExprFString { @@ -456,8 +456,8 @@ Module( FString { range: 279..302, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 283..293, expression: StringLiteral( ExprStringLiteral { @@ -483,7 +483,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 293..299, value: " inner", }, @@ -506,7 +506,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 303..309, value: " outer", }, @@ -538,13 +538,13 @@ Module( range: 336..359, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 338..343, value: "test ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 343..353, expression: Name( ExprName { @@ -559,7 +559,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 353..358, value: " more", }, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep750_t_string_py314.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep750_t_string_py314.py.snap new file mode 100644 index 0000000000..3d322265ab --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep750_t_string_py314.py.snap @@ -0,0 +1,584 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/pep750_t_string_py314.py +--- +## AST + +``` +Module( + ModModule { + range: 0..403, + body: [ + Expr( + StmtExpr { + range: 44..74, + value: TString( + ExprTString { + range: 44..74, + value: TStringValue { + inner: Single( + TString( + TString { + range: 44..74, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 46..58, + value: "Magic wand: ", + }, + ), + Interpolation( + InterpolatedElement { + range: 58..73, + expression: Subscript( + ExprSubscript { + range: 60..71, + value: Name( + ExprName { + range: 60..63, + id: Name("bag"), + ctx: Load, + }, + ), + slice: StringLiteral( + ExprStringLiteral { + range: 64..70, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 64..70, + value: "wand", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 95..112, + value: TString( + ExprTString { + range: 95..112, + value: TStringValue { + inner: Single( + TString( + TString { + range: 95..112, + elements: [ + Interpolation( + InterpolatedElement { + range: 97..111, + expression: Call( + ExprCall { + range: 98..110, + func: Attribute( + ExprAttribute { + range: 98..107, + value: StringLiteral( + ExprStringLiteral { + range: 98..102, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 98..102, + value: "\n", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + attr: Identifier { + id: Name("join"), + range: 103..107, + }, + ctx: Load, + }, + ), + arguments: Arguments { + range: 107..110, + args: [ + Name( + ExprName { + range: 108..109, + id: Name("a"), + ctx: Load, + }, + ), + ], + keywords: [], + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 148..220, + value: TString( + ExprTString { + range: 148..220, + value: TStringValue { + inner: Single( + TString( + TString { + range: 148..220, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 152..169, + value: "A complex trick: ", + }, + ), + Interpolation( + InterpolatedElement { + range: 169..217, + expression: Subscript( + ExprSubscript { + range: 175..185, + value: Name( + ExprName { + range: 175..178, + id: Name("bag"), + ctx: Load, + }, + ), + slice: StringLiteral( + ExprStringLiteral { + range: 179..184, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 179..184, + value: "bag", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 221..254, + value: TString( + ExprTString { + range: 221..254, + value: TStringValue { + inner: Single( + TString( + TString { + range: 221..254, + elements: [ + Interpolation( + InterpolatedElement { + range: 223..253, + expression: TString( + ExprTString { + range: 224..252, + value: TStringValue { + inner: Single( + TString( + TString { + range: 224..252, + elements: [ + Interpolation( + InterpolatedElement { + range: 226..251, + expression: TString( + ExprTString { + range: 227..250, + value: TStringValue { + inner: Single( + TString( + TString { + range: 227..250, + elements: [ + Interpolation( + InterpolatedElement { + range: 229..249, + expression: TString( + ExprTString { + range: 230..248, + value: TStringValue { + inner: Single( + TString( + TString { + range: 230..248, + elements: [ + Interpolation( + InterpolatedElement { + range: 232..247, + expression: TString( + ExprTString { + range: 233..246, + value: TStringValue { + inner: Single( + TString( + TString { + range: 233..246, + elements: [ + Interpolation( + InterpolatedElement { + range: 235..245, + expression: TString( + ExprTString { + range: 236..244, + value: TStringValue { + inner: Single( + TString( + TString { + range: 236..244, + elements: [ + Interpolation( + InterpolatedElement { + range: 238..243, + expression: BinOp( + ExprBinOp { + range: 239..242, + left: NumberLiteral( + ExprNumberLiteral { + range: 239..240, + value: Int( + 1, + ), + }, + ), + op: Add, + right: NumberLiteral( + ExprNumberLiteral { + range: 241..242, + value: Int( + 1, + ), + }, + ), + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 276..310, + value: TString( + ExprTString { + range: 276..310, + value: TStringValue { + inner: Single( + TString( + TString { + range: 276..310, + elements: [ + Interpolation( + InterpolatedElement { + range: 278..303, + expression: TString( + ExprTString { + range: 279..302, + value: TStringValue { + inner: Single( + TString( + TString { + range: 279..302, + elements: [ + Interpolation( + InterpolatedElement { + range: 283..293, + expression: StringLiteral( + ExprStringLiteral { + range: 284..292, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 284..292, + value: "nested", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 293..299, + value: " inner", + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 303..309, + value: " outer", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 336..359, + value: TString( + ExprTString { + range: 336..359, + value: TStringValue { + inner: Single( + TString( + TString { + range: 336..359, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 338..343, + value: "test ", + }, + ), + Interpolation( + InterpolatedElement { + range: 343..353, + expression: Name( + ExprName { + range: 344..345, + id: Name("a"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 353..358, + value: " more", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__ambiguous_lpar_with_items.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__ambiguous_lpar_with_items.py.snap index 48d299be48..b651e7c557 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__ambiguous_lpar_with_items.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__ambiguous_lpar_with_items.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/statement/ambiguous_lpar_with_items.py -snapshot_kind: text --- ## AST @@ -945,8 +944,8 @@ Module( FString { range: 1186..1201, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 1188..1200, expression: Name( ExprName { @@ -958,11 +957,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 1195..1199, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 1195..1199, value: "= 42", }, @@ -1017,8 +1016,8 @@ Module( FString { range: 1214..1231, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 1216..1230, expression: Named( ExprNamed { diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__match.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__match.py.snap index ec88a56393..eb24f71df1 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__match.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__match.py.snap @@ -3767,8 +3767,8 @@ Module( FString { range: 2932..2938, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2934..2937, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__try.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__try.py.snap index 3f799c4d6b..2413f26d90 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__try.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__try.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/statement/try.py -snapshot_kind: text --- ## AST @@ -577,13 +576,13 @@ Module( range: 505..524, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 507..514, value: "caught ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 514..523, expression: Call( ExprCall { @@ -682,13 +681,13 @@ Module( range: 557..576, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 559..566, value: "caught ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 566..575, expression: Call( ExprCall { @@ -955,13 +954,13 @@ Module( range: 704..750, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 706..713, value: "caught ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 713..722, expression: Call( ExprCall { @@ -994,13 +993,13 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 722..735, value: " with nested ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 735..749, expression: Attribute( ExprAttribute { @@ -1091,13 +1090,13 @@ Module( range: 784..830, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 786..793, value: "caught ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 793..802, expression: Call( ExprCall { @@ -1130,13 +1129,13 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 802..815, value: " with nested ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 815..829, expression: Attribute( ExprAttribute { diff --git a/crates/ruff_python_semantic/src/analyze/type_inference.rs b/crates/ruff_python_semantic/src/analyze/type_inference.rs index d25e0f7e7a..c0f6f6d482 100644 --- a/crates/ruff_python_semantic/src/analyze/type_inference.rs +++ b/crates/ruff_python_semantic/src/analyze/type_inference.rs @@ -78,6 +78,7 @@ impl From<&Expr> for ResolvedPythonType { Expr::Tuple(_) => ResolvedPythonType::Atom(PythonType::Tuple), Expr::Generator(_) => ResolvedPythonType::Atom(PythonType::Generator), Expr::FString(_) => ResolvedPythonType::Atom(PythonType::String), + Expr::TString(_) => ResolvedPythonType::Unknown, Expr::StringLiteral(_) => ResolvedPythonType::Atom(PythonType::String), Expr::BytesLiteral(_) => ResolvedPythonType::Atom(PythonType::Bytes), Expr::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => match value { diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 76ef4a8617..043d98cff5 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -1939,10 +1939,15 @@ impl<'a> SemanticModel<'a> { self.flags.intersects(SemanticModelFlags::F_STRING) } + /// Return `true` if the model is in a t-string. + pub const fn in_t_string(&self) -> bool { + self.flags.intersects(SemanticModelFlags::T_STRING) + } + /// Return `true` if the model is in an f-string replacement field. - pub const fn in_f_string_replacement_field(&self) -> bool { + pub const fn in_interpolated_string_replacement_field(&self) -> bool { self.flags - .intersects(SemanticModelFlags::F_STRING_REPLACEMENT_FIELD) + .intersects(SemanticModelFlags::INTERPOLATED_STRING_REPLACEMENT_FIELD) } /// Return `true` if the model is in boolean test. @@ -2461,7 +2466,7 @@ bitflags! { /// ```python /// f"first {x} second {y}" /// ``` - const F_STRING_REPLACEMENT_FIELD = 1 << 21; + const INTERPOLATED_STRING_REPLACEMENT_FIELD = 1 << 21; /// The model is visiting the bases tuple of a class. /// @@ -2549,6 +2554,15 @@ bitflags! { /// [#13824]: https://github.com/astral-sh/ruff/issues/13824 const NO_TYPE_CHECK = 1 << 28; + /// The model is in a t-string. + /// + /// For example, the model could be visiting `x` in: + /// ```python + /// t'{x}' + /// ``` + const T_STRING = 1 << 29; + + /// The context is in any type annotation. const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_EVALUATED_ANNOTATION.bits() | Self::RUNTIME_REQUIRED_ANNOTATION.bits(); diff --git a/crates/ty_python_semantic/src/semantic_index/re_exports.rs b/crates/ty_python_semantic/src/semantic_index/re_exports.rs index b8002dbf0b..049e751bf0 100644 --- a/crates/ty_python_semantic/src/semantic_index/re_exports.rs +++ b/crates/ty_python_semantic/src/semantic_index/re_exports.rs @@ -325,6 +325,7 @@ impl<'db> Visitor<'db> for ExportFinder<'db> { | ast::Expr::Yield(_) | ast::Expr::YieldFrom(_) | ast::Expr::FString(_) + | ast::Expr::TString(_) | ast::Expr::Tuple(_) | ast::Expr::List(_) | ast::Expr::Slice(_) @@ -389,6 +390,7 @@ impl<'db> Visitor<'db> for WalrusFinder<'_, 'db> { | ast::Expr::Yield(_) | ast::Expr::YieldFrom(_) | ast::Expr::FString(_) + | ast::Expr::TString(_) | ast::Expr::Tuple(_) | ast::Expr::List(_) | ast::Expr::Slice(_) diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index 9ed888e603..e3e7b15cf8 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -116,6 +116,7 @@ impl_expression_has_type!(ast::ExprYieldFrom); impl_expression_has_type!(ast::ExprCompare); impl_expression_has_type!(ast::ExprCall); impl_expression_has_type!(ast::ExprFString); +impl_expression_has_type!(ast::ExprTString); impl_expression_has_type!(ast::ExprStringLiteral); impl_expression_has_type!(ast::ExprBytesLiteral); impl_expression_has_type!(ast::ExprNumberLiteral); @@ -152,6 +153,7 @@ impl HasType for ast::Expr { Expr::Compare(inner) => inner.inferred_type(model), Expr::Call(inner) => inner.inferred_type(model), Expr::FString(inner) => inner.inferred_type(model), + Expr::TString(inner) => inner.inferred_type(model), Expr::StringLiteral(inner) => inner.inferred_type(model), Expr::BytesLiteral(inner) => inner.inferred_type(model), Expr::NumberLiteral(inner) => inner.inferred_type(model), diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 73aefafb3c..acbf27b881 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -4328,6 +4328,7 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_bytes_literal_expression(bytes_literal) } ast::Expr::FString(fstring) => self.infer_fstring_expression(fstring), + ast::Expr::TString(tstring) => self.infer_tstring_expression(tstring), ast::Expr::EllipsisLiteral(literal) => self.infer_ellipsis_literal_expression(literal), ast::Expr::Tuple(tuple) => self.infer_tuple_expression(tuple), ast::Expr::List(list) => self.infer_list_expression(list), @@ -4426,8 +4427,8 @@ impl<'db> TypeInferenceBuilder<'db> { ast::FStringPart::FString(fstring) => { for element in &fstring.elements { match element { - ast::FStringElement::Expression(expression) => { - let ast::FStringExpressionElement { + ast::InterpolatedStringElement::Interpolation(expression) => { + let ast::InterpolatedElement { range: _, expression, debug_text: _, @@ -4437,7 +4438,7 @@ impl<'db> TypeInferenceBuilder<'db> { let ty = self.infer_expression(expression); if let Some(format_spec) = format_spec { - for element in format_spec.elements.expressions() { + for element in format_spec.elements.interpolations() { self.infer_expression(&element.expression); } } @@ -4456,7 +4457,7 @@ impl<'db> TypeInferenceBuilder<'db> { } } } - ast::FStringElement::Literal(literal) => { + ast::InterpolatedStringElement::Literal(literal) => { collector.push_str(&literal.value); } } @@ -4467,6 +4468,59 @@ impl<'db> TypeInferenceBuilder<'db> { collector.string_type(self.db()) } + fn infer_tstring_expression(&mut self, tstring: &ast::ExprTString) -> Type<'db> { + let ast::ExprTString { value, .. } = tstring; + for part in value { + match part { + ast::TStringPart::Literal(_) => {} + ast::TStringPart::FString(fstring) => { + for element in &fstring.elements { + match element { + ast::InterpolatedStringElement::Interpolation(expression) => { + let ast::InterpolatedElement { + expression, + format_spec, + .. + } = expression; + self.infer_expression(expression); + + if let Some(format_spec) = format_spec { + for element in format_spec.elements.interpolations() { + self.infer_expression(&element.expression); + } + } + } + ast::InterpolatedStringElement::Literal(_) => {} + } + } + } + ast::TStringPart::TString(tstring) => { + for element in &tstring.elements { + match element { + ast::InterpolatedStringElement::Interpolation( + tstring_interpolation_element, + ) => { + let ast::InterpolatedElement { + expression, + format_spec, + .. + } = tstring_interpolation_element; + self.infer_expression(expression); + if let Some(format_spec) = format_spec { + for element in format_spec.elements.interpolations() { + self.infer_expression(&element.expression); + } + } + } + ast::InterpolatedStringElement::Literal(_) => {} + } + } + } + } + } + todo_type!("Template") + } + fn infer_ellipsis_literal_expression( &mut self, _literal: &ast::ExprEllipsisLiteral, @@ -8285,6 +8339,14 @@ impl<'db> TypeInferenceBuilder<'db> { ) } + ast::Expr::TString(tstring) => { + self.infer_tstring_expression(tstring); + self.report_invalid_type_expression( + expression, + format_args!("T-strings are not allowed in type expressions"), + ) + } + ast::Expr::Slice(slice) => { self.infer_slice_expression(slice); self.report_invalid_type_expression(