From dcf31c93480a026bc51d6a8b0c3c4d72ee079cd3 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 18 Mar 2025 11:12:15 -0400 Subject: [PATCH] [syntax-errors] PEP 701 f-strings before Python 3.12 (#16543) ## Summary This PR detects the use of PEP 701 f-strings before 3.12. This one sounded difficult and ended up being pretty easy, so I think there's a good chance I've over-simplified things. However, from experimenting in the Python REPL and checking with [pyright], I think this is correct. pyright actually doesn't even flag the comment case, but Python does. I also checked pyright's implementation for [quotes](https://github.com/microsoft/pyright/blob/98dc4469cc5126bcdcbaf396a6c9d7e75dc1c4a0/packages/pyright-internal/src/analyzer/checker.ts#L1379-L1398) and [escapes](https://github.com/microsoft/pyright/blob/98dc4469cc5126bcdcbaf396a6c9d7e75dc1c4a0/packages/pyright-internal/src/analyzer/checker.ts#L1365-L1377) and think I've approximated how they do it. Python's error messages also point to the simple approach of these characters simply not being allowed: ```pycon Python 3.11.11 (main, Feb 12 2025, 14:51:05) [Clang 19.1.6 ] on linux Type "help", "copyright", "credits" or "license" for more information. >>> f'''multiline { ... expression # comment ... }''' File "", line 3 }''' ^ SyntaxError: f-string expression part cannot include '#' >>> f'''{not a line \ ... continuation}''' File "", line 2 continuation}''' ^ SyntaxError: f-string expression part cannot include a backslash >>> f'hello {'world'}' File "", line 1 f'hello {'world'}' ^^^^^ SyntaxError: f-string: expecting '}' ``` And since escapes aren't allowed, I don't think there are any tricky cases where nested quotes or comments can sneak in. It's also slightly annoying that the error is repeated for every nested quote character, but that also mirrors pyright, although they highlight the whole nested string, which is a little nicer. However, their check is in the analysis phase, so I don't think we have such easy access to the quoted range, at least without adding another mini visitor. ## Test Plan New inline tests [pyright]: https://pyright-play.net/?pythonVersion=3.11&strict=true&code=EYQw5gBAvBAmCWBjALgCgO4gHaygRgEoAoEaCAIgBpyiiBiCLAUwGdknYIBHAVwHt2LIgDMA5AFlwSCJhwAuCAG8IoMAG1Rs2KIC6EAL6iIxosbPmLlq5foRWiEAAcmERAAsQAJxAomnltY2wuSKogA6WKIAdABWfPBYqCAE%2BuSBVqbpWVm2iHwAtvlMWMgB2ekiolUAgq4FjgA2TAAeEMieSADWCsoV5qoaqrrGDJ5MiDz%2B8ABuLqosAIREhlXlaybrmyYMXsDw7V4AnoysyAmQ5SIhwYo3d9cheADUeKlv5O%2BpQA --- .../fixtures/black/cases/fstring.options.json | 1 + .../inline/err/pep701_f_string_py311.py | 12 + .../inline/ok/pep701_f_string_py311.py | 7 + .../inline/ok/pep701_f_string_py312.py | 10 + crates/ruff_python_parser/src/error.rs | 46 + .../src/parser/expression.rs | 84 +- crates/ruff_python_parser/src/token.rs | 6 + crates/ruff_python_parser/src/token_source.rs | 15 + ...valid_syntax@pep701_f_string_py311.py.snap | 929 ++++++++++++++++++ ...valid_syntax@pep701_f_string_py311.py.snap | 532 ++++++++++ ...valid_syntax@pep701_f_string_py312.py.snap | 584 +++++++++++ 11 files changed, 2223 insertions(+), 3 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/cases/fstring.options.json create mode 100644 crates/ruff_python_parser/resources/inline/err/pep701_f_string_py311.py create mode 100644 crates/ruff_python_parser/resources/inline/ok/pep701_f_string_py311.py create mode 100644 crates/ruff_python_parser/resources/inline/ok/pep701_f_string_py312.py create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep701_f_string_py311.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py311.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py312.py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/cases/fstring.options.json b/crates/ruff_python_formatter/resources/test/fixtures/black/cases/fstring.options.json new file mode 100644 index 0000000000..a97114e048 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/cases/fstring.options.json @@ -0,0 +1 @@ +{"target_version": "3.12"} diff --git a/crates/ruff_python_parser/resources/inline/err/pep701_f_string_py311.py b/crates/ruff_python_parser/resources/inline/err/pep701_f_string_py311.py new file mode 100644 index 0000000000..91d8d8a6c4 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/pep701_f_string_py311.py @@ -0,0 +1,12 @@ +# parse_options: {"target-version": "3.11"} +f'Magic wand: { bag['wand'] }' # nested quotes +f"{'\n'.join(a)}" # escape sequence +f'''A complex trick: { + bag['bag'] # comment +}''' +f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting +f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes +f"test {a \ + } more" # line continuation +f"""{f"""{x}"""}""" # mark the whole triple quote +f"{'\n'.join(['\t', '\v', '\r'])}" # multiple escape sequences, multiple errors diff --git a/crates/ruff_python_parser/resources/inline/ok/pep701_f_string_py311.py b/crates/ruff_python_parser/resources/inline/ok/pep701_f_string_py311.py new file mode 100644 index 0000000000..40c0958df6 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/pep701_f_string_py311.py @@ -0,0 +1,7 @@ +# parse_options: {"target-version": "3.11"} +f"outer {'# not a comment'}" +f'outer {x:{"# not a comment"} }' +f"""{f'''{f'{"# not a comment"}'}'''}""" +f"""{f'''# before expression {f'# aro{f"#{1+1}#"}und #'}'''} # after expression""" +f"escape outside of \t {expr}\n" +f"test\"abcd" diff --git a/crates/ruff_python_parser/resources/inline/ok/pep701_f_string_py312.py b/crates/ruff_python_parser/resources/inline/ok/pep701_f_string_py312.py new file mode 100644 index 0000000000..8a8b7a469d --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/pep701_f_string_py312.py @@ -0,0 +1,10 @@ +# parse_options: {"target-version": "3.12"} +f'Magic wand: { bag['wand'] }' # nested quotes +f"{'\n'.join(a)}" # escape sequence +f'''A complex trick: { + bag['bag'] # comment +}''' +f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting +f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes +f"test {a \ + } more" # line continuation diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index 46eada457d..a4611b8da0 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -452,6 +452,14 @@ pub enum StarTupleKind { Yield, } +/// The type of PEP 701 f-string error for [`UnsupportedSyntaxErrorKind::Pep701FString`]. +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +pub enum FStringKind { + Backslash, + Comment, + NestedQuote, +} + #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] pub enum UnparenthesizedNamedExprKind { SequenceIndex, @@ -661,6 +669,34 @@ pub enum UnsupportedSyntaxErrorKind { TypeAliasStatement, TypeParamDefault, + /// Represents the use of a [PEP 701] f-string before Python 3.12. + /// + /// ## Examples + /// + /// As described in the PEP, each of these cases were invalid before Python 3.12: + /// + /// ```python + /// # nested quotes + /// f'Magic wand: { bag['wand'] }' + /// + /// # escape characters + /// f"{'\n'.join(a)}" + /// + /// # comments + /// f'''A complex trick: { + /// bag['bag'] # recursive bags! + /// }''' + /// + /// # arbitrary nesting + /// f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" + /// ``` + /// + /// These restrictions were lifted in Python 3.12, meaning that all of these examples are now + /// valid. + /// + /// [PEP 701]: https://peps.python.org/pep-0701/ + Pep701FString(FStringKind), + /// Represents the use of a parenthesized `with` item before Python 3.9. /// /// ## Examples @@ -838,6 +874,15 @@ impl Display for UnsupportedSyntaxError { UnsupportedSyntaxErrorKind::TypeParamDefault => { "Cannot set default type for a type parameter" } + UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::Backslash) => { + "Cannot use an escape sequence (backslash) in f-strings" + } + UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::Comment) => { + "Cannot use comments in f-strings" + } + UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::NestedQuote) => { + "Cannot reuse outer quote character in f-strings" + } UnsupportedSyntaxErrorKind::ParenthesizedContextManager => { "Cannot use parentheses within a `with` statement" } @@ -904,6 +949,7 @@ impl UnsupportedSyntaxErrorKind { UnsupportedSyntaxErrorKind::TypeParameterList => Change::Added(PythonVersion::PY312), UnsupportedSyntaxErrorKind::TypeAliasStatement => Change::Added(PythonVersion::PY312), UnsupportedSyntaxErrorKind::TypeParamDefault => Change::Added(PythonVersion::PY313), + UnsupportedSyntaxErrorKind::Pep701FString(_) => Change::Added(PythonVersion::PY312), UnsupportedSyntaxErrorKind::ParenthesizedContextManager => { Change::Added(PythonVersion::PY39) } diff --git a/crates/ruff_python_parser/src/parser/expression.rs b/crates/ruff_python_parser/src/parser/expression.rs index 43ff8881af..a090d1525e 100644 --- a/crates/ruff_python_parser/src/parser/expression.rs +++ b/crates/ruff_python_parser/src/parser/expression.rs @@ -11,13 +11,15 @@ use ruff_python_ast::{ }; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; -use crate::error::{StarTupleKind, UnparenthesizedNamedExprKind}; +use crate::error::{FStringKind, StarTupleKind, UnparenthesizedNamedExprKind}; use crate::parser::progress::ParserProgress; use crate::parser::{helpers, FunctionKind, Parser}; use crate::string::{parse_fstring_literal_element, parse_string_literal, StringType}; use crate::token::{TokenKind, TokenValue}; use crate::token_set::TokenSet; -use crate::{FStringErrorType, Mode, ParseErrorType, UnsupportedSyntaxErrorKind}; +use crate::{ + FStringErrorType, Mode, ParseErrorType, UnsupportedSyntaxError, UnsupportedSyntaxErrorKind, +}; use super::{FStringElementsKind, Parenthesized, RecoveryContextKind}; @@ -1393,13 +1395,89 @@ impl<'src> Parser<'src> { self.expect(TokenKind::FStringEnd); + // test_ok pep701_f_string_py312 + // # parse_options: {"target-version": "3.12"} + // f'Magic wand: { bag['wand'] }' # nested quotes + // f"{'\n'.join(a)}" # escape sequence + // f'''A complex trick: { + // bag['bag'] # comment + // }''' + // f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting + // f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes + // f"test {a \ + // } more" # line continuation + + // test_ok pep701_f_string_py311 + // # parse_options: {"target-version": "3.11"} + // f"outer {'# not a comment'}" + // f'outer {x:{"# not a comment"} }' + // f"""{f'''{f'{"# not a comment"}'}'''}""" + // f"""{f'''# before expression {f'# aro{f"#{1+1}#"}und #'}'''} # after expression""" + // f"escape outside of \t {expr}\n" + // f"test\"abcd" + + // test_err pep701_f_string_py311 + // # parse_options: {"target-version": "3.11"} + // f'Magic wand: { bag['wand'] }' # nested quotes + // f"{'\n'.join(a)}" # escape sequence + // f'''A complex trick: { + // bag['bag'] # comment + // }''' + // f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting + // f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes + // f"test {a \ + // } more" # line continuation + // f"""{f"""{x}"""}""" # mark the whole triple quote + // f"{'\n'.join(['\t', '\v', '\r'])}" # multiple escape sequences, multiple errors + + let range = self.node_range(start); + + if !self.options.target_version.supports_pep_701() { + let quote_bytes = flags.quote_str().as_bytes(); + let quote_len = flags.quote_len(); + for expr in elements.expressions() { + for slash_position in memchr::memchr_iter(b'\\', self.source[expr.range].as_bytes()) + { + let slash_position = TextSize::try_from(slash_position).unwrap(); + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::Backslash), + TextRange::at(expr.range.start() + slash_position, '\\'.text_len()), + ); + } + + if let Some(quote_position) = + memchr::memmem::find(self.source[expr.range].as_bytes(), quote_bytes) + { + let quote_position = TextSize::try_from(quote_position).unwrap(); + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::NestedQuote), + TextRange::at(expr.range.start() + quote_position, quote_len), + ); + }; + } + + self.check_fstring_comments(range); + } + ast::FString { elements, - range: self.node_range(start), + range, flags: ast::FStringFlags::from(flags), } } + /// Check `range` for comment tokens and report an `UnsupportedSyntaxError` for each one found. + fn check_fstring_comments(&mut self, range: TextRange) { + self.unsupported_syntax_errors + .extend(self.tokens.in_range(range).iter().filter_map(|token| { + token.kind().is_comment().then_some(UnsupportedSyntaxError { + kind: UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::Comment), + range: token.range(), + target_version: self.options.target_version, + }) + })); + } + /// Parses a list of f-string elements. /// /// # Panics diff --git a/crates/ruff_python_parser/src/token.rs b/crates/ruff_python_parser/src/token.rs index 9574e4c23c..193aecac51 100644 --- a/crates/ruff_python_parser/src/token.rs +++ b/crates/ruff_python_parser/src/token.rs @@ -418,6 +418,12 @@ impl TokenKind { matches!(self, TokenKind::Comment | TokenKind::NonLogicalNewline) } + /// Returns `true` if this is a comment token. + #[inline] + pub const fn is_comment(&self) -> bool { + matches!(self, TokenKind::Comment) + } + #[inline] pub const fn is_arithmetic(self) -> bool { matches!( diff --git a/crates/ruff_python_parser/src/token_source.rs b/crates/ruff_python_parser/src/token_source.rs index 4851879c89..8b379af4c2 100644 --- a/crates/ruff_python_parser/src/token_source.rs +++ b/crates/ruff_python_parser/src/token_source.rs @@ -166,6 +166,21 @@ impl<'src> TokenSource<'src> { self.tokens.truncate(tokens_position); } + /// Returns a slice of [`Token`] that are within the given `range`. + pub(crate) fn in_range(&self, range: TextRange) -> &[Token] { + let start = self + .tokens + .iter() + .rposition(|tok| tok.start() == range.start()); + let end = self.tokens.iter().rposition(|tok| tok.end() == range.end()); + + let (Some(start), Some(end)) = (start, end) else { + return &self.tokens; + }; + + &self.tokens[start..=end] + } + /// Consumes the token source, returning the collected tokens, comment ranges, and any errors /// encountered during lexing. The token collection includes both the trivia and non-trivia /// tokens. 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 new file mode 100644 index 0000000000..5ac816ecc8 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep701_f_string_py311.py.snap @@ -0,0 +1,929 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/pep701_f_string_py311.py +--- +## AST + +``` +Module( + ModModule { + range: 0..549, + body: [ + Expr( + StmtExpr { + range: 44..74, + value: FString( + ExprFString { + range: 44..74, + value: FStringValue { + inner: Single( + FString( + FString { + range: 44..74, + elements: [ + Literal( + FStringLiteralElement { + range: 46..58, + value: "Magic wand: ", + }, + ), + Expression( + FStringExpressionElement { + 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: FStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 95..112, + value: FString( + ExprFString { + range: 95..112, + value: FStringValue { + inner: Single( + FString( + FString { + range: 95..112, + elements: [ + Expression( + FStringExpressionElement { + 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: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 148..220, + value: FString( + ExprFString { + range: 148..220, + value: FStringValue { + inner: Single( + FString( + FString { + range: 148..220, + elements: [ + Literal( + FStringLiteralElement { + range: 152..169, + value: "A complex trick: ", + }, + ), + Expression( + FStringExpressionElement { + 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: FStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 221..254, + value: FString( + ExprFString { + range: 221..254, + value: FStringValue { + inner: Single( + FString( + FString { + range: 221..254, + elements: [ + Expression( + FStringExpressionElement { + range: 223..253, + expression: FString( + ExprFString { + range: 224..252, + value: FStringValue { + inner: Single( + FString( + FString { + range: 224..252, + elements: [ + Expression( + FStringExpressionElement { + range: 226..251, + expression: FString( + ExprFString { + range: 227..250, + value: FStringValue { + inner: Single( + FString( + FString { + range: 227..250, + elements: [ + Expression( + FStringExpressionElement { + range: 229..249, + expression: FString( + ExprFString { + range: 230..248, + value: FStringValue { + inner: Single( + FString( + FString { + range: 230..248, + elements: [ + Expression( + FStringExpressionElement { + range: 232..247, + expression: FString( + ExprFString { + range: 233..246, + value: FStringValue { + inner: Single( + FString( + FString { + range: 233..246, + elements: [ + Expression( + FStringExpressionElement { + range: 235..245, + expression: FString( + ExprFString { + range: 236..244, + value: FStringValue { + inner: Single( + FString( + FString { + range: 236..244, + elements: [ + Expression( + FStringExpressionElement { + 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: FStringFlags { + 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: FStringFlags { + 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: FStringFlags { + 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, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 276..310, + value: FString( + ExprFString { + range: 276..310, + value: FStringValue { + inner: Single( + FString( + FString { + range: 276..310, + elements: [ + Expression( + FStringExpressionElement { + range: 278..303, + expression: FString( + ExprFString { + range: 279..302, + value: FStringValue { + inner: Single( + FString( + FString { + range: 279..302, + elements: [ + Expression( + FStringExpressionElement { + 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( + FStringLiteralElement { + range: 293..299, + value: " inner", + }, + ), + ], + flags: FStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + FStringLiteralElement { + range: 303..309, + value: " outer", + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 336..359, + value: FString( + ExprFString { + range: 336..359, + value: FStringValue { + inner: Single( + FString( + FString { + range: 336..359, + elements: [ + Literal( + FStringLiteralElement { + range: 338..343, + value: "test ", + }, + ), + Expression( + FStringExpressionElement { + range: 343..353, + expression: Name( + ExprName { + range: 344..345, + id: Name("a"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + FStringLiteralElement { + range: 353..358, + value: " more", + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 403..422, + value: FString( + ExprFString { + range: 403..422, + value: FStringValue { + inner: Single( + FString( + FString { + range: 403..422, + elements: [ + Expression( + FStringExpressionElement { + range: 407..419, + expression: FString( + ExprFString { + range: 408..418, + value: FStringValue { + inner: Single( + FString( + FString { + range: 408..418, + elements: [ + Expression( + FStringExpressionElement { + range: 412..415, + expression: Name( + ExprName { + range: 413..414, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 468..502, + value: FString( + ExprFString { + range: 468..502, + value: FStringValue { + inner: Single( + FString( + FString { + range: 468..502, + elements: [ + Expression( + FStringExpressionElement { + range: 470..501, + expression: Call( + ExprCall { + range: 471..500, + func: Attribute( + ExprAttribute { + range: 471..480, + value: StringLiteral( + ExprStringLiteral { + range: 471..475, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 471..475, + value: "\n", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + attr: Identifier { + id: Name("join"), + range: 476..480, + }, + ctx: Load, + }, + ), + arguments: Arguments { + range: 480..500, + args: [ + List( + ExprList { + range: 481..499, + elts: [ + StringLiteral( + ExprStringLiteral { + range: 482..486, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 482..486, + value: "\t", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + StringLiteral( + ExprStringLiteral { + range: 488..492, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 488..492, + value: "\u{b}", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + StringLiteral( + ExprStringLiteral { + range: 494..498, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 494..498, + value: "\r", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + ], + ctx: Load, + }, + ), + ], + keywords: [], + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` +## Unsupported Syntax Errors + + | +1 | # parse_options: {"target-version": "3.11"} +2 | f'Magic wand: { bag['wand'] }' # nested quotes + | ^ Syntax Error: Cannot reuse outer quote character in f-strings on Python 3.11 (syntax was added in Python 3.12) +3 | f"{'\n'.join(a)}" # escape sequence +4 | f'''A complex trick: { + | + + + | +1 | # parse_options: {"target-version": "3.11"} +2 | f'Magic wand: { bag['wand'] }' # nested quotes +3 | f"{'\n'.join(a)}" # escape sequence + | ^ Syntax Error: Cannot use an escape sequence (backslash) in f-strings on Python 3.11 (syntax was added in Python 3.12) +4 | f'''A complex trick: { +5 | bag['bag'] # comment + | + + + | +3 | f"{'\n'.join(a)}" # escape sequence +4 | f'''A complex trick: { +5 | bag['bag'] # comment + | ^^^^^^^^^ Syntax Error: Cannot use comments in f-strings on Python 3.11 (syntax was added in Python 3.12) +6 | }''' +7 | f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting + | + + + | +5 | bag['bag'] # comment +6 | }''' +7 | f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting + | ^ Syntax Error: Cannot reuse outer quote character in f-strings on Python 3.11 (syntax was added in Python 3.12) +8 | f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes +9 | f"test {a \ + | + + + | +5 | bag['bag'] # comment +6 | }''' +7 | f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting + | ^ Syntax Error: Cannot reuse outer quote character in f-strings on Python 3.11 (syntax was added in Python 3.12) +8 | f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes +9 | f"test {a \ + | + + + | +5 | bag['bag'] # comment +6 | }''' +7 | f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting + | ^ Syntax Error: Cannot reuse outer quote character in f-strings on Python 3.11 (syntax was added in Python 3.12) +8 | f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes +9 | f"test {a \ + | + + + | +5 | bag['bag'] # comment +6 | }''' +7 | f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting + | ^ Syntax Error: Cannot reuse outer quote character in f-strings on Python 3.11 (syntax was added in Python 3.12) +8 | f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes +9 | f"test {a \ + | + + + | +5 | bag['bag'] # comment +6 | }''' +7 | f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting + | ^ Syntax Error: Cannot reuse outer quote character in f-strings on Python 3.11 (syntax was added in Python 3.12) +8 | f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes +9 | f"test {a \ + | + + + | + 6 | }''' + 7 | f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting + 8 | f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes + | ^ Syntax Error: Cannot reuse outer quote character in f-strings on Python 3.11 (syntax was added in Python 3.12) + 9 | f"test {a \ +10 | } more" # line continuation + | + + + | + 7 | f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting + 8 | f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes + 9 | f"test {a \ + | ^ Syntax Error: Cannot use an escape sequence (backslash) in f-strings on Python 3.11 (syntax was added in Python 3.12) +10 | } more" # line continuation +11 | f"""{f"""{x}"""}""" # mark the whole triple quote + | + + + | + 9 | f"test {a \ +10 | } more" # line continuation +11 | f"""{f"""{x}"""}""" # mark the whole triple quote + | ^^^ Syntax Error: Cannot reuse outer quote character in f-strings on Python 3.11 (syntax was added in Python 3.12) +12 | f"{'\n'.join(['\t', '\v', '\r'])}" # multiple escape sequences, multiple errors + | + + + | +10 | } more" # line continuation +11 | f"""{f"""{x}"""}""" # mark the whole triple quote +12 | f"{'\n'.join(['\t', '\v', '\r'])}" # multiple escape sequences, multiple errors + | ^ Syntax Error: Cannot use an escape sequence (backslash) in f-strings on Python 3.11 (syntax was added in Python 3.12) + | + + + | +10 | } more" # line continuation +11 | f"""{f"""{x}"""}""" # mark the whole triple quote +12 | f"{'\n'.join(['\t', '\v', '\r'])}" # multiple escape sequences, multiple errors + | ^ Syntax Error: Cannot use an escape sequence (backslash) in f-strings on Python 3.11 (syntax was added in Python 3.12) + | + + + | +10 | } more" # line continuation +11 | f"""{f"""{x}"""}""" # mark the whole triple quote +12 | f"{'\n'.join(['\t', '\v', '\r'])}" # multiple escape sequences, multiple errors + | ^ Syntax Error: Cannot use an escape sequence (backslash) in f-strings on Python 3.11 (syntax was added in Python 3.12) + | + + + | +10 | } more" # line continuation +11 | f"""{f"""{x}"""}""" # mark the whole triple quote +12 | f"{'\n'.join(['\t', '\v', '\r'])}" # multiple escape sequences, multiple errors + | ^ Syntax Error: Cannot use an escape sequence (backslash) in f-strings on Python 3.11 (syntax was added in Python 3.12) + | 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 new file mode 100644 index 0000000000..d4dcb42151 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py311.py.snap @@ -0,0 +1,532 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/pep701_f_string_py311.py +--- +## AST + +``` +Module( + ModModule { + range: 0..278, + body: [ + Expr( + StmtExpr { + range: 44..72, + value: FString( + ExprFString { + range: 44..72, + value: FStringValue { + inner: Single( + FString( + FString { + range: 44..72, + elements: [ + Literal( + FStringLiteralElement { + range: 46..52, + value: "outer ", + }, + ), + Expression( + FStringExpressionElement { + range: 52..71, + expression: StringLiteral( + ExprStringLiteral { + range: 53..70, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 53..70, + value: "# not a comment", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 73..106, + value: FString( + ExprFString { + range: 73..106, + value: FStringValue { + inner: Single( + FString( + FString { + range: 73..106, + elements: [ + Literal( + FStringLiteralElement { + range: 75..81, + value: "outer ", + }, + ), + Expression( + FStringExpressionElement { + range: 81..105, + expression: Name( + ExprName { + range: 82..83, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + FStringFormatSpec { + range: 84..104, + elements: [ + Expression( + FStringExpressionElement { + range: 84..103, + expression: StringLiteral( + ExprStringLiteral { + range: 85..102, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 85..102, + value: "# not a comment", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + FStringLiteralElement { + range: 103..104, + value: " ", + }, + ), + ], + }, + ), + }, + ), + ], + flags: FStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 107..147, + value: FString( + ExprFString { + range: 107..147, + value: FStringValue { + inner: Single( + FString( + FString { + range: 107..147, + elements: [ + Expression( + FStringExpressionElement { + range: 111..144, + expression: FString( + ExprFString { + range: 112..143, + value: FStringValue { + inner: Single( + FString( + FString { + range: 112..143, + elements: [ + Expression( + FStringExpressionElement { + range: 116..140, + expression: FString( + ExprFString { + range: 117..139, + value: FStringValue { + inner: Single( + FString( + FString { + range: 117..139, + elements: [ + Expression( + FStringExpressionElement { + range: 119..138, + expression: StringLiteral( + ExprStringLiteral { + range: 120..137, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 120..137, + value: "# not a comment", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 148..230, + value: FString( + ExprFString { + range: 148..230, + value: FStringValue { + inner: Single( + FString( + FString { + range: 148..230, + elements: [ + Expression( + FStringExpressionElement { + range: 152..208, + expression: FString( + ExprFString { + range: 153..207, + value: FStringValue { + inner: Single( + FString( + FString { + range: 153..207, + elements: [ + Literal( + FStringLiteralElement { + range: 157..177, + value: "# before expression ", + }, + ), + Expression( + FStringExpressionElement { + range: 177..204, + expression: FString( + ExprFString { + range: 178..203, + value: FStringValue { + inner: Single( + FString( + FString { + range: 178..203, + elements: [ + Literal( + FStringLiteralElement { + range: 180..185, + value: "# aro", + }, + ), + Expression( + FStringExpressionElement { + range: 185..197, + expression: FString( + ExprFString { + range: 186..196, + value: FStringValue { + inner: Single( + FString( + FString { + range: 186..196, + elements: [ + Literal( + FStringLiteralElement { + range: 188..189, + value: "#", + }, + ), + Expression( + FStringExpressionElement { + range: 189..194, + expression: BinOp( + ExprBinOp { + range: 190..193, + left: NumberLiteral( + ExprNumberLiteral { + range: 190..191, + value: Int( + 1, + ), + }, + ), + op: Add, + right: NumberLiteral( + ExprNumberLiteral { + range: 192..193, + value: Int( + 1, + ), + }, + ), + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + FStringLiteralElement { + range: 194..195, + value: "#", + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + FStringLiteralElement { + range: 197..202, + value: "und #", + }, + ), + ], + flags: FStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + FStringLiteralElement { + range: 208..227, + value: " # after expression", + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 231..263, + value: FString( + ExprFString { + range: 231..263, + value: FStringValue { + inner: Single( + FString( + FString { + range: 231..263, + elements: [ + Literal( + FStringLiteralElement { + range: 233..254, + value: "escape outside of \t ", + }, + ), + Expression( + FStringExpressionElement { + range: 254..260, + expression: Name( + ExprName { + range: 255..259, + id: Name("expr"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + FStringLiteralElement { + range: 260..262, + value: "\n", + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 264..277, + value: FString( + ExprFString { + range: 264..277, + value: FStringValue { + inner: Single( + FString( + FString { + range: 264..277, + elements: [ + Literal( + FStringLiteralElement { + range: 266..276, + value: "test\"abcd", + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` 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 new file mode 100644 index 0000000000..c9eea80822 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py312.py.snap @@ -0,0 +1,584 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/pep701_f_string_py312.py +--- +## AST + +``` +Module( + ModModule { + range: 0..403, + body: [ + Expr( + StmtExpr { + range: 44..74, + value: FString( + ExprFString { + range: 44..74, + value: FStringValue { + inner: Single( + FString( + FString { + range: 44..74, + elements: [ + Literal( + FStringLiteralElement { + range: 46..58, + value: "Magic wand: ", + }, + ), + Expression( + FStringExpressionElement { + 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: FStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 95..112, + value: FString( + ExprFString { + range: 95..112, + value: FStringValue { + inner: Single( + FString( + FString { + range: 95..112, + elements: [ + Expression( + FStringExpressionElement { + 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: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 148..220, + value: FString( + ExprFString { + range: 148..220, + value: FStringValue { + inner: Single( + FString( + FString { + range: 148..220, + elements: [ + Literal( + FStringLiteralElement { + range: 152..169, + value: "A complex trick: ", + }, + ), + Expression( + FStringExpressionElement { + 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: FStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 221..254, + value: FString( + ExprFString { + range: 221..254, + value: FStringValue { + inner: Single( + FString( + FString { + range: 221..254, + elements: [ + Expression( + FStringExpressionElement { + range: 223..253, + expression: FString( + ExprFString { + range: 224..252, + value: FStringValue { + inner: Single( + FString( + FString { + range: 224..252, + elements: [ + Expression( + FStringExpressionElement { + range: 226..251, + expression: FString( + ExprFString { + range: 227..250, + value: FStringValue { + inner: Single( + FString( + FString { + range: 227..250, + elements: [ + Expression( + FStringExpressionElement { + range: 229..249, + expression: FString( + ExprFString { + range: 230..248, + value: FStringValue { + inner: Single( + FString( + FString { + range: 230..248, + elements: [ + Expression( + FStringExpressionElement { + range: 232..247, + expression: FString( + ExprFString { + range: 233..246, + value: FStringValue { + inner: Single( + FString( + FString { + range: 233..246, + elements: [ + Expression( + FStringExpressionElement { + range: 235..245, + expression: FString( + ExprFString { + range: 236..244, + value: FStringValue { + inner: Single( + FString( + FString { + range: 236..244, + elements: [ + Expression( + FStringExpressionElement { + 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: FStringFlags { + 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: FStringFlags { + 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: FStringFlags { + 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, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 276..310, + value: FString( + ExprFString { + range: 276..310, + value: FStringValue { + inner: Single( + FString( + FString { + range: 276..310, + elements: [ + Expression( + FStringExpressionElement { + range: 278..303, + expression: FString( + ExprFString { + range: 279..302, + value: FStringValue { + inner: Single( + FString( + FString { + range: 279..302, + elements: [ + Expression( + FStringExpressionElement { + 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( + FStringLiteralElement { + range: 293..299, + value: " inner", + }, + ), + ], + flags: FStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + FStringLiteralElement { + range: 303..309, + value: " outer", + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 336..359, + value: FString( + ExprFString { + range: 336..359, + value: FStringValue { + inner: Single( + FString( + FString { + range: 336..359, + elements: [ + Literal( + FStringLiteralElement { + range: 338..343, + value: "test ", + }, + ), + Expression( + FStringExpressionElement { + range: 343..353, + expression: Name( + ExprName { + range: 344..345, + id: Name("a"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + FStringLiteralElement { + range: 353..358, + value: " more", + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +```