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, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +```