diff --git a/crates/ruff_python_parser/src/parser/expression.rs b/crates/ruff_python_parser/src/parser/expression.rs index b176d1e319401e..bf8773705495c4 100644 --- a/crates/ruff_python_parser/src/parser/expression.rs +++ b/crates/ruff_python_parser/src/parser/expression.rs @@ -1788,7 +1788,7 @@ impl<'src> Parser<'src> { let spec_start = self.node_start(); let elements = self.parse_interpolated_string_elements( flags, - InterpolatedStringElementsKind::FormatSpec, + InterpolatedStringElementsKind::FormatSpec(string_kind), string_kind, ); Some(Box::new(ast::InterpolatedStringFormatSpec { diff --git a/crates/ruff_python_parser/src/parser/mod.rs b/crates/ruff_python_parser/src/parser/mod.rs index 8d0614b6b918fd..b826b561a9e071 100644 --- a/crates/ruff_python_parser/src/parser/mod.rs +++ b/crates/ruff_python_parser/src/parser/mod.rs @@ -817,7 +817,7 @@ enum InterpolatedStringElementsKind { /// ```py /// f"hello {x:.2f} world" /// ``` - FormatSpec, + FormatSpec(InterpolatedStringKind), } impl InterpolatedStringElementsKind { @@ -827,7 +827,7 @@ impl InterpolatedStringElementsKind { // test_ok fstring_format_spec_terminator // f"hello {x:} world" // f"hello {x:.3f} world" - InterpolatedStringElementsKind::FormatSpec => TokenKind::Rbrace, + InterpolatedStringElementsKind::FormatSpec(_) => TokenKind::Rbrace, } } } @@ -1197,23 +1197,13 @@ impl RecoveryContextKind { ) || p.at_name_or_soft_keyword() } RecoveryContextKind::WithItems(_) => p.at_expr(), - RecoveryContextKind::InterpolatedStringElements(elements_kind) => { - match elements_kind { - InterpolatedStringElementsKind::Regular(interpolated_string_kind) => { - p.current_token_kind() == interpolated_string_kind.middle_token() - || p.current_token_kind() == TokenKind::Lbrace - } - InterpolatedStringElementsKind::FormatSpec => { - matches!( - p.current_token_kind(), - // Literal element - TokenKind::FStringMiddle | TokenKind::TStringMiddle - // Expression element - | TokenKind::Lbrace - ) - } + RecoveryContextKind::InterpolatedStringElements(elements_kind) => match elements_kind { + InterpolatedStringElementsKind::Regular(interpolated_string_kind) + | InterpolatedStringElementsKind::FormatSpec(interpolated_string_kind) => { + p.current_token_kind() == interpolated_string_kind.middle_token() + || p.current_token_kind() == TokenKind::Lbrace } - } + }, } } @@ -1305,9 +1295,11 @@ impl RecoveryContextKind { InterpolatedStringElementsKind::Regular(string_kind) => ParseErrorType::OtherError( format!("Expected an element of or the end of the {string_kind}"), ), - InterpolatedStringElementsKind::FormatSpec => ParseErrorType::OtherError( - "Expected an f-string or t-string element or a '}'".to_string(), - ), + InterpolatedStringElementsKind::FormatSpec(string_kind) => { + ParseErrorType::OtherError(format!( + "Expected an {string_kind} element or a '}}'" + )) + } }, } } @@ -1345,10 +1337,11 @@ bitflags! { const LAMBDA_PARAMETERS = 1 << 24; 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 T_STRING_ELEMENTS = 1 << 30; - const FT_STRING_ELEMENTS_IN_FORMAT_SPEC = 1 << 31; + const WITH_ITEMS_UNPARENTHESIZED = 1 << 27; + const F_STRING_ELEMENTS = 1 << 28; + const T_STRING_ELEMENTS = 1 << 29; + const F_STRING_ELEMENTS_IN_FORMAT_SPEC = 1 << 30; + const T_STRING_ELEMENTS_IN_FORMAT_SPEC = 1 << 31; } } @@ -1409,8 +1402,11 @@ impl RecoveryContext { RecoveryContext::T_STRING_ELEMENTS } - InterpolatedStringElementsKind::FormatSpec => { - RecoveryContext::FT_STRING_ELEMENTS_IN_FORMAT_SPEC + InterpolatedStringElementsKind::FormatSpec(InterpolatedStringKind::FString) => { + RecoveryContext::F_STRING_ELEMENTS_IN_FORMAT_SPEC + } + InterpolatedStringElementsKind::FormatSpec(InterpolatedStringKind::TString) => { + RecoveryContext::T_STRING_ELEMENTS_IN_FORMAT_SPEC } }, } @@ -1485,9 +1481,14 @@ impl RecoveryContext { RecoveryContext::T_STRING_ELEMENTS => RecoveryContextKind::InterpolatedStringElements( InterpolatedStringElementsKind::Regular(InterpolatedStringKind::TString), ), - RecoveryContext::FT_STRING_ELEMENTS_IN_FORMAT_SPEC => { + RecoveryContext::F_STRING_ELEMENTS_IN_FORMAT_SPEC => { + RecoveryContextKind::InterpolatedStringElements( + InterpolatedStringElementsKind::FormatSpec(InterpolatedStringKind::FString), + ) + } + RecoveryContext::T_STRING_ELEMENTS_IN_FORMAT_SPEC => { RecoveryContextKind::InterpolatedStringElements( - InterpolatedStringElementsKind::FormatSpec, + InterpolatedStringElementsKind::FormatSpec(InterpolatedStringKind::TString), ) } _ => return None, diff --git a/crates/ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__tstring_fstring_middle.snap b/crates/ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__tstring_fstring_middle.snap new file mode 100644 index 00000000000000..12c9787631e776 --- /dev/null +++ b/crates/ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__tstring_fstring_middle.snap @@ -0,0 +1,8 @@ +--- +source: crates/ruff_python_parser/src/parser/tests.rs +expression: error +--- +ParseError { + error: ExpectedExpression, + location: 3..4, +} diff --git a/crates/ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__tstring_fstring_middle_fuzzer.snap b/crates/ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__tstring_fstring_middle_fuzzer.snap new file mode 100644 index 00000000000000..e26bcbc33e9f04 --- /dev/null +++ b/crates/ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__tstring_fstring_middle_fuzzer.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff_python_parser/src/parser/tests.rs +expression: error +--- +ParseError { + error: Lexical( + UnrecognizedToken { + tok: '\0', + }, + ), + location: 5..6, +} diff --git a/crates/ruff_python_parser/src/parser/tests.rs b/crates/ruff_python_parser/src/parser/tests.rs index dcb9ac16a0af58..fb1927775479e6 100644 --- a/crates/ruff_python_parser/src/parser/tests.rs +++ b/crates/ruff_python_parser/src/parser/tests.rs @@ -157,3 +157,23 @@ t"i}'"#; insta::assert_debug_snapshot!(error); } + +#[test] +fn test_tstring_fstring_middle() { + let source = "t'{:{F'{\0}F"; + let parsed = parse_expression(source); + + let error = parsed.unwrap_err(); + + insta::assert_debug_snapshot!(error); +} + +#[test] +fn test_tstring_fstring_middle_fuzzer() { + let source = "A1[A\u{c}\0:+,>1t'{:f\0:{f\"f\0:\0{fm\0:{f:\u{10}\0\0\0:bb\0{@f>f\u{1}'\0f"; + let parsed = parse_expression(source); + + let error = parsed.unwrap_err(); + + insta::assert_debug_snapshot!(error); +}