Catch syntax errors in nested interpolations before Python 3.12 (#20949)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (${{ github.repository == 'astral-sh/ruff' && 'depot-windows-2022-16' || 'windows-latest' }}) (push) Blocked by required conditions
CI / cargo test (macos-latest) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / ty completion evaluation (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks instrumented (ruff) (push) Blocked by required conditions
CI / benchmarks instrumented (ty) (push) Blocked by required conditions
CI / benchmarks walltime (medium|multithreaded) (push) Blocked by required conditions
CI / benchmarks walltime (small|large) (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

Summary
--

This PR fixes the issue I added in #20867 and noticed in #20930. Cases
like this
cause an error on any Python version:

```py
f"{1:""}"
```

which gave me a false sense of security before. Cases like this are
still
invalid only before 3.12 and weren't flagged after the changes in
#20867:

```py
f'{1: abcd "{'aa'}" }'
           # ^  reused quote
f'{1: abcd "{"\n"}" }'
            # ^  backslash
```

I didn't recognize these as nested interpolations that also need to be
checked
for invalid expressions, so filtering out the whole format spec wasn't
quite
right. And `elements.interpolations()` only iterates over the outermost 
interpolations, not the nested ones.

There's basically no code change in this PR, I just moved the existing
check
from `parse_interpolated_string`, which parses the entire string, to
`parse_interpolated_element`. This kind of seems more natural anyway and
avoids
having to try to recursively visit nested elements after the fact in
`parse_interpolated_string`. So viewing the diff with something like

```
git diff --color-moved --ignore-space-change --color-moved-ws=allow-indentation-change main
```

should make this more clear.

Test Plan
--

New tests
This commit is contained in:
Brent Westbrook 2025-10-20 09:03:13 -04:00 committed by GitHub
parent c2ae9c7806
commit 38c074e67d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 335 additions and 96 deletions

View file

@ -0,0 +1,231 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/pep701_nested_interpolation_py311.py
---
## AST
```
Module(
ModModule {
node_index: NodeIndex(None),
range: 0..138,
body: [
Expr(
StmtExpr {
node_index: NodeIndex(None),
range: 92..114,
value: FString(
ExprFString {
node_index: NodeIndex(None),
range: 92..114,
value: FStringValue {
inner: Single(
FString(
FString {
range: 92..114,
node_index: NodeIndex(None),
elements: [
Interpolation(
InterpolatedElement {
range: 94..113,
node_index: NodeIndex(None),
expression: NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 95..96,
value: Int(
1,
),
},
),
debug_text: None,
conversion: None,
format_spec: Some(
InterpolatedStringFormatSpec {
range: 97..112,
node_index: NodeIndex(None),
elements: [
Literal(
InterpolatedStringLiteralElement {
range: 97..104,
node_index: NodeIndex(None),
value: " abcd \"",
},
),
Interpolation(
InterpolatedElement {
range: 104..110,
node_index: NodeIndex(None),
expression: StringLiteral(
ExprStringLiteral {
node_index: NodeIndex(None),
range: 105..109,
value: StringLiteralValue {
inner: Single(
StringLiteral {
range: 105..109,
node_index: NodeIndex(None),
value: "aa",
flags: StringLiteralFlags {
quote_style: Single,
prefix: Empty,
triple_quoted: false,
unclosed: false,
},
},
),
},
},
),
debug_text: None,
conversion: None,
format_spec: None,
},
),
Literal(
InterpolatedStringLiteralElement {
range: 110..112,
node_index: NodeIndex(None),
value: "\" ",
},
),
],
},
),
},
),
],
flags: FStringFlags {
quote_style: Single,
prefix: Regular,
triple_quoted: false,
unclosed: false,
},
},
),
),
},
},
),
},
),
Expr(
StmtExpr {
node_index: NodeIndex(None),
range: 115..137,
value: FString(
ExprFString {
node_index: NodeIndex(None),
range: 115..137,
value: FStringValue {
inner: Single(
FString(
FString {
range: 115..137,
node_index: NodeIndex(None),
elements: [
Interpolation(
InterpolatedElement {
range: 117..136,
node_index: NodeIndex(None),
expression: NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 118..119,
value: Int(
1,
),
},
),
debug_text: None,
conversion: None,
format_spec: Some(
InterpolatedStringFormatSpec {
range: 120..135,
node_index: NodeIndex(None),
elements: [
Literal(
InterpolatedStringLiteralElement {
range: 120..127,
node_index: NodeIndex(None),
value: " abcd \"",
},
),
Interpolation(
InterpolatedElement {
range: 127..133,
node_index: NodeIndex(None),
expression: StringLiteral(
ExprStringLiteral {
node_index: NodeIndex(None),
range: 128..132,
value: StringLiteralValue {
inner: Single(
StringLiteral {
range: 128..132,
node_index: NodeIndex(None),
value: "\n",
flags: StringLiteralFlags {
quote_style: Double,
prefix: Empty,
triple_quoted: false,
unclosed: false,
},
},
),
},
},
),
debug_text: None,
conversion: None,
format_spec: None,
},
),
Literal(
InterpolatedStringLiteralElement {
range: 133..135,
node_index: NodeIndex(None),
value: "\" ",
},
),
],
},
),
},
),
],
flags: FStringFlags {
quote_style: Single,
prefix: Regular,
triple_quoted: false,
unclosed: false,
},
},
),
),
},
},
),
},
),
],
},
)
```
## Unsupported Syntax Errors
|
1 | # parse_options: {"target-version": "3.11"}
2 | # nested interpolations also need to be checked
3 | f'{1: abcd "{'aa'}" }'
| ^ Syntax Error: Cannot reuse outer quote character in f-strings on Python 3.11 (syntax was added in Python 3.12)
4 | f'{1: abcd "{"\n"}" }'
|
|
2 | # nested interpolations also need to be checked
3 | f'{1: abcd "{'aa'}" }'
4 | f'{1: abcd "{"\n"}" }'
| ^ Syntax Error: Cannot use an escape sequence (backslash) in f-strings on Python 3.11 (syntax was added in Python 3.12)
|