Fix syntax error false positives for escapes and quotes in f-strings (#20867)

Summary
--

Fixes #20844 by refining the unsupported syntax error check for [PEP
701]
f-strings before Python 3.12 to allow backslash escapes and escaped
outer quotes
in the format spec part of f-strings. These are only disallowed within
the
f-string expression part on earlier versions. Using the examples from
the PR:

```pycon
>>> f"{1:\x64}"
'1'
>>> f"{1:\"d\"}"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: Invalid format specifier '"d"' for object of type 'int'
```

Note that the second case is a runtime error, but this is actually
avoidable if
you override `__format__`, so despite being pretty weird, this could
actually be
a valid use case.

```pycon
>>> class C:
...     def __format__(*args, **kwargs): return "<C>"
...
>>> f"{C():\"d\"}"
'<C>'
```

At first I thought narrowing the range we check to exclude the format
spec would
only work for escapes, but it turns out that cases like `f"{1:""}"` are
already
covered by an existing `ParseError`, so we can just narrow the range of
both our
escape and quote checks.

Our comment check also seems to be working correctly because it's based
on the
actual tokens. A case like
[this](https://play.ruff.rs/9f1c2ff2-cd8e-4ad7-9f40-56c0a524209f):

```python
f"""{1:# }"""
```

doesn't include a comment token, instead the `#` is part of an
`InterpolatedStringLiteralElement`.

Test Plan
--

New inline parser tests

[PEP 701]: https://peps.python.org/pep-0701/
This commit is contained in:
Brent Westbrook 2025-10-15 09:23:16 -04:00 committed by GitHub
parent 8817ea5c84
commit 8b9ab48ac6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 317 additions and 62 deletions

View file

@ -0,0 +1,90 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/nested_quote_in_format_spec_py312.py
---
## AST
```
Module(
ModModule {
node_index: NodeIndex(None),
range: 0..94,
body: [
Expr(
StmtExpr {
node_index: NodeIndex(None),
range: 44..53,
value: FString(
ExprFString {
node_index: NodeIndex(None),
range: 44..53,
value: FStringValue {
inner: Concatenated(
[
FString(
FString {
range: 44..50,
node_index: NodeIndex(None),
elements: [
Interpolation(
InterpolatedElement {
range: 46..49,
node_index: NodeIndex(None),
expression: NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 47..48,
value: Int(
1,
),
},
),
debug_text: None,
conversion: None,
format_spec: Some(
InterpolatedStringFormatSpec {
range: 49..49,
node_index: NodeIndex(None),
elements: [],
},
),
},
),
],
flags: FStringFlags {
quote_style: Double,
prefix: Regular,
triple_quoted: false,
},
},
),
Literal(
StringLiteral {
range: 50..53,
node_index: NodeIndex(None),
value: "}",
flags: StringLiteralFlags {
quote_style: Double,
prefix: Empty,
triple_quoted: false,
},
},
),
],
),
},
},
),
},
),
],
},
)
```
## Errors
|
1 | # parse_options: {"target-version": "3.12"}
2 | f"{1:""}" # this is a ParseError on all versions
| ^ Syntax Error: f-string: expecting '}'
|

View file

@ -0,0 +1,77 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/ok/non_nested_quote_in_format_spec_py311.py
---
## AST
```
Module(
ModModule {
node_index: NodeIndex(None),
range: 0..90,
body: [
Expr(
StmtExpr {
node_index: NodeIndex(None),
range: 44..53,
value: FString(
ExprFString {
node_index: NodeIndex(None),
range: 44..53,
value: FStringValue {
inner: Single(
FString(
FString {
range: 44..53,
node_index: NodeIndex(None),
elements: [
Interpolation(
InterpolatedElement {
range: 46..52,
node_index: NodeIndex(None),
expression: NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 47..48,
value: Int(
1,
),
},
),
debug_text: None,
conversion: None,
format_spec: Some(
InterpolatedStringFormatSpec {
range: 49..51,
node_index: NodeIndex(None),
elements: [
Literal(
InterpolatedStringLiteralElement {
range: 49..51,
node_index: NodeIndex(None),
value: "''",
},
),
],
},
),
},
),
],
flags: FStringFlags {
quote_style: Double,
prefix: Regular,
triple_quoted: false,
},
},
),
),
},
},
),
},
),
],
},
)
```

View file

@ -8,7 +8,7 @@ input_file: crates/ruff_python_parser/resources/inline/ok/pep701_f_string_py311.
Module(
ModModule {
node_index: NodeIndex(None),
range: 0..278,
range: 0..398,
body: [
Expr(
StmtExpr {
@ -604,6 +604,128 @@ Module(
),
},
),
Expr(
StmtExpr {
node_index: NodeIndex(None),
range: 278..289,
value: FString(
ExprFString {
node_index: NodeIndex(None),
range: 278..289,
value: FStringValue {
inner: Single(
FString(
FString {
range: 278..289,
node_index: NodeIndex(None),
elements: [
Interpolation(
InterpolatedElement {
range: 280..288,
node_index: NodeIndex(None),
expression: NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 281..282,
value: Int(
1,
),
},
),
debug_text: None,
conversion: None,
format_spec: Some(
InterpolatedStringFormatSpec {
range: 283..287,
node_index: NodeIndex(None),
elements: [
Literal(
InterpolatedStringLiteralElement {
range: 283..287,
node_index: NodeIndex(None),
value: "d",
},
),
],
},
),
},
),
],
flags: FStringFlags {
quote_style: Double,
prefix: Regular,
triple_quoted: false,
},
},
),
),
},
},
),
},
),
Expr(
StmtExpr {
node_index: NodeIndex(None),
range: 330..342,
value: FString(
ExprFString {
node_index: NodeIndex(None),
range: 330..342,
value: FStringValue {
inner: Single(
FString(
FString {
range: 330..342,
node_index: NodeIndex(None),
elements: [
Interpolation(
InterpolatedElement {
range: 332..341,
node_index: NodeIndex(None),
expression: NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 333..334,
value: Int(
1,
),
},
),
debug_text: None,
conversion: None,
format_spec: Some(
InterpolatedStringFormatSpec {
range: 335..340,
node_index: NodeIndex(None),
elements: [
Literal(
InterpolatedStringLiteralElement {
range: 335..340,
node_index: NodeIndex(None),
value: "\"d\"",
},
),
],
},
),
},
),
],
flags: FStringFlags {
quote_style: Double,
prefix: Regular,
triple_quoted: false,
},
},
),
),
},
},
),
},
),
],
},
)