[`parser`] Fix panic when parsing IPython escape command expressions (#21480)

## Summary

Fixes a panic when parsing IPython escape commands with `Help` kind
(`?`) in expression contexts. The parser now reports an error instead of
panicking.

Fixes #21465.

## Problem

The parser panicked with `unreachable!()` in
`parse_ipython_escape_command_expression` when encountering escape
commands with `Help` kind (`?`) in expression contexts, where only
`Magic` (`%`) and `Shell` (`!`) are allowed.

## Approach

Replaced the `unreachable!()` panic with error handling that adds a
`ParseErrorType::OtherError` and continues parsing, returning a valid
AST node with the error attached.

## Test Plan

Added `test_ipython_escape_command_in_with_statement` and
`test_ipython_help_escape_command_as_expression` to verify the fix.

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
This commit is contained in:
Dan Parizher 2025-11-24 00:40:27 -05:00 committed by GitHub
parent 3b23d3c041
commit 474b00568a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 307 additions and 1 deletions

View File

@ -0,0 +1,3 @@
# parse_options: {"mode": "ipython"}
with (a, ?b)
?

View File

@ -0,0 +1,3 @@
# parse_options: {"mode": "ipython"}
with (a, ?b
?

View File

@ -0,0 +1,4 @@
# parse_options: {"mode": "ipython"}
with a, ?b
?
x = 1

View File

@ -1115,7 +1115,27 @@ impl RecoveryContextKind {
TokenKind::Colon => Some(ListTerminatorKind::ErrorRecovery),
_ => None,
},
WithItemKind::Unparenthesized | WithItemKind::ParenthesizedExpression => p
// test_err ipython_help_escape_command_error_recovery_1
// # parse_options: {"mode": "ipython"}
// with (a, ?b)
// ?
// test_err ipython_help_escape_command_error_recovery_2
// # parse_options: {"mode": "ipython"}
// with (a, ?b
// ?
// test_err ipython_help_escape_command_error_recovery_3
// # parse_options: {"mode": "ipython"}
// with a, ?b
// ?
// x = 1
WithItemKind::Unparenthesized => matches!(
p.current_token_kind(),
TokenKind::Colon | TokenKind::Newline
)
.then_some(ListTerminatorKind::Regular),
WithItemKind::ParenthesizedExpression => p
.at(TokenKind::Colon)
.then_some(ListTerminatorKind::Regular),
},

View File

@ -0,0 +1,84 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/ipython_help_escape_command_error_recovery_1.py
---
## AST
```
Module(
ModModule {
node_index: NodeIndex(None),
range: 0..52,
body: [
With(
StmtWith {
node_index: NodeIndex(None),
range: 37..49,
is_async: false,
items: [
WithItem {
range: 43..44,
node_index: NodeIndex(None),
context_expr: Name(
ExprName {
node_index: NodeIndex(None),
range: 43..44,
id: Name("a"),
ctx: Load,
},
),
optional_vars: None,
},
WithItem {
range: 47..48,
node_index: NodeIndex(None),
context_expr: Name(
ExprName {
node_index: NodeIndex(None),
range: 47..48,
id: Name("b"),
ctx: Load,
},
),
optional_vars: None,
},
],
body: [],
},
),
IpyEscapeCommand(
StmtIpyEscapeCommand {
node_index: NodeIndex(None),
range: 50..51,
kind: Help,
value: "",
},
),
],
},
)
```
## Errors
|
1 | # parse_options: {"mode": "ipython"}
2 | with (a, ?b)
| ^ Syntax Error: Expected `,`, found `?`
3 | ?
|
|
1 | # parse_options: {"mode": "ipython"}
2 | with (a, ?b)
| ^ Syntax Error: Expected `:`, found newline
3 | ?
|
|
1 | # parse_options: {"mode": "ipython"}
2 | with (a, ?b)
3 | ?
| ^ Syntax Error: Expected an indented block after `with` statement
|

View File

@ -0,0 +1,80 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/ipython_help_escape_command_error_recovery_2.py
---
## AST
```
Module(
ModModule {
node_index: NodeIndex(None),
range: 0..51,
body: [
With(
StmtWith {
node_index: NodeIndex(None),
range: 37..51,
is_async: false,
items: [
WithItem {
range: 42..51,
node_index: NodeIndex(None),
context_expr: Tuple(
ExprTuple {
node_index: NodeIndex(None),
range: 42..51,
elts: [
Name(
ExprName {
node_index: NodeIndex(None),
range: 43..44,
id: Name("a"),
ctx: Load,
},
),
Name(
ExprName {
node_index: NodeIndex(None),
range: 47..48,
id: Name("b"),
ctx: Load,
},
),
],
ctx: Load,
parenthesized: true,
},
),
optional_vars: None,
},
],
body: [],
},
),
],
},
)
```
## Errors
|
1 | # parse_options: {"mode": "ipython"}
2 | with (a, ?b
| ^ Syntax Error: Expected an expression or a ')'
3 | ?
|
|
1 | # parse_options: {"mode": "ipython"}
2 | with (a, ?b
3 | ?
| ^ Syntax Error: Expected `,`, found `?`
|
|
2 | with (a, ?b
3 | ?
| ^ Syntax Error: unexpected EOF while parsing
|

View File

@ -0,0 +1,112 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/ipython_help_escape_command_error_recovery_3.py
---
## AST
```
Module(
ModModule {
node_index: NodeIndex(None),
range: 0..56,
body: [
With(
StmtWith {
node_index: NodeIndex(None),
range: 37..47,
is_async: false,
items: [
WithItem {
range: 42..43,
node_index: NodeIndex(None),
context_expr: Name(
ExprName {
node_index: NodeIndex(None),
range: 42..43,
id: Name("a"),
ctx: Load,
},
),
optional_vars: None,
},
WithItem {
range: 46..47,
node_index: NodeIndex(None),
context_expr: Name(
ExprName {
node_index: NodeIndex(None),
range: 46..47,
id: Name("b"),
ctx: Load,
},
),
optional_vars: None,
},
],
body: [],
},
),
IpyEscapeCommand(
StmtIpyEscapeCommand {
node_index: NodeIndex(None),
range: 48..49,
kind: Help,
value: "",
},
),
Assign(
StmtAssign {
node_index: NodeIndex(None),
range: 50..55,
targets: [
Name(
ExprName {
node_index: NodeIndex(None),
range: 50..51,
id: Name("x"),
ctx: Store,
},
),
],
value: NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 54..55,
value: Int(
1,
),
},
),
},
),
],
},
)
```
## Errors
|
1 | # parse_options: {"mode": "ipython"}
2 | with a, ?b
| ^ Syntax Error: Expected `,`, found `?`
3 | ?
4 | x = 1
|
|
1 | # parse_options: {"mode": "ipython"}
2 | with a, ?b
| ^ Syntax Error: Expected `:`, found newline
3 | ?
4 | x = 1
|
|
1 | # parse_options: {"mode": "ipython"}
2 | with a, ?b
3 | ?
| ^ Syntax Error: Expected an indented block after `with` statement
4 | x = 1
|