Skip to content

Commit 6c14225

Browse files
authored
[syntax-errors] Tuple unpacking in return and yield before Python 3.8 (#16485)
Summary -- Checks for tuple unpacking in `return` and `yield` statements before Python 3.8, as described [here]. Test Plan -- Inline tests. [here]: python/cpython#76298
1 parent 0a627ef commit 6c14225

15 files changed

+1217
-8
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# parse_options: {"target-version": "3.7"}
2+
rest = (4, 5, 6)
3+
def f(): return 1, 2, 3, *rest
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# parse_options: {"target-version": "3.7"}
2+
rest = (4, 5, 6)
3+
def g(): yield 1, 2, 3, *rest
4+
def h(): yield 1, (yield 2, *rest), 3
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# parse_options: {"target-version": "3.7"}
2+
rest = (4, 5, 6)
3+
def f(): return (1, 2, 3, *rest)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# parse_options: {"target-version": "3.8"}
2+
rest = (4, 5, 6)
3+
def f(): return 1, 2, 3, *rest
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# parse_options: {"target-version": "3.7"}
2+
rest = (4, 5, 6)
3+
def g(): yield (1, 2, 3, *rest)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# parse_options: {"target-version": "3.8"}
2+
rest = (4, 5, 6)
3+
def g(): yield 1, 2, 3, *rest
4+
def h(): yield 1, (yield 2, *rest), 3

crates/ruff_python_parser/src/error.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,11 +444,68 @@ pub struct UnsupportedSyntaxError {
444444
pub target_version: PythonVersion,
445445
}
446446

447+
/// The type of tuple unpacking for [`UnsupportedSyntaxErrorKind::StarTuple`].
448+
#[derive(Debug, PartialEq, Clone, Copy)]
449+
pub enum StarTupleKind {
450+
Return,
451+
Yield,
452+
}
453+
447454
#[derive(Debug, PartialEq, Clone, Copy)]
448455
pub enum UnsupportedSyntaxErrorKind {
449456
Match,
450457
Walrus,
451458
ExceptStar,
459+
460+
/// Represents the use of unparenthesized tuple unpacking in a `return` statement or `yield`
461+
/// expression before Python 3.8.
462+
///
463+
/// ## Examples
464+
///
465+
/// Before Python 3.8, this syntax was allowed:
466+
///
467+
/// ```python
468+
/// rest = (4, 5, 6)
469+
///
470+
/// def f():
471+
/// t = 1, 2, 3, *rest
472+
/// return t
473+
///
474+
/// def g():
475+
/// t = 1, 2, 3, *rest
476+
/// yield t
477+
/// ```
478+
///
479+
/// But this was not:
480+
///
481+
/// ```python
482+
/// rest = (4, 5, 6)
483+
///
484+
/// def f():
485+
/// return 1, 2, 3, *rest
486+
///
487+
/// def g():
488+
/// yield 1, 2, 3, *rest
489+
/// ```
490+
///
491+
/// Instead, parentheses were required in the `return` and `yield` cases:
492+
///
493+
/// ```python
494+
/// rest = (4, 5, 6)
495+
///
496+
/// def f():
497+
/// return (1, 2, 3, *rest)
498+
///
499+
/// def g():
500+
/// yield (1, 2, 3, *rest)
501+
/// ```
502+
///
503+
/// This was reported in [BPO 32117] and updated in Python 3.8 to allow the unparenthesized
504+
/// form.
505+
///
506+
/// [BPO 32117]: https://github.com/python/cpython/issues/76298
507+
StarTuple(StarTupleKind),
508+
452509
/// Represents the use of a "relaxed" [PEP 614] decorator before Python 3.9.
453510
///
454511
/// ## Examples
@@ -480,6 +537,7 @@ pub enum UnsupportedSyntaxErrorKind {
480537
/// [`dotted_name`]: https://docs.python.org/3.8/reference/compound_stmts.html#grammar-token-dotted-name
481538
/// [decorator grammar]: https://docs.python.org/3/reference/compound_stmts.html#grammar-token-python-grammar-decorator
482539
RelaxedDecorator,
540+
483541
/// Represents the use of a [PEP 570] positional-only parameter before Python 3.8.
484542
///
485543
/// ## Examples
@@ -506,6 +564,7 @@ pub enum UnsupportedSyntaxErrorKind {
506564
///
507565
/// [PEP 570]: https://peps.python.org/pep-0570/
508566
PositionalOnlyParameter,
567+
509568
/// Represents the use of a [type parameter list] before Python 3.12.
510569
///
511570
/// ## Examples
@@ -544,6 +603,12 @@ impl Display for UnsupportedSyntaxError {
544603
UnsupportedSyntaxErrorKind::Match => "Cannot use `match` statement",
545604
UnsupportedSyntaxErrorKind::Walrus => "Cannot use named assignment expression (`:=`)",
546605
UnsupportedSyntaxErrorKind::ExceptStar => "Cannot use `except*`",
606+
UnsupportedSyntaxErrorKind::StarTuple(StarTupleKind::Return) => {
607+
"Cannot use iterable unpacking in return statements"
608+
}
609+
UnsupportedSyntaxErrorKind::StarTuple(StarTupleKind::Yield) => {
610+
"Cannot use iterable unpacking in yield expressions"
611+
}
547612
UnsupportedSyntaxErrorKind::RelaxedDecorator => "Unsupported expression in decorators",
548613
UnsupportedSyntaxErrorKind::PositionalOnlyParameter => {
549614
"Cannot use positional-only parameter separator"
@@ -570,6 +635,7 @@ impl UnsupportedSyntaxErrorKind {
570635
UnsupportedSyntaxErrorKind::Match => PythonVersion::PY310,
571636
UnsupportedSyntaxErrorKind::Walrus => PythonVersion::PY38,
572637
UnsupportedSyntaxErrorKind::ExceptStar => PythonVersion::PY311,
638+
UnsupportedSyntaxErrorKind::StarTuple(_) => PythonVersion::PY38,
573639
UnsupportedSyntaxErrorKind::RelaxedDecorator => PythonVersion::PY39,
574640
UnsupportedSyntaxErrorKind::PositionalOnlyParameter => PythonVersion::PY38,
575641
UnsupportedSyntaxErrorKind::TypeParameterList => PythonVersion::PY312,

crates/ruff_python_parser/src/parser/expression.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use ruff_python_ast::{
1111
};
1212
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
1313

14+
use crate::error::StarTupleKind;
1415
use crate::parser::progress::ParserProgress;
1516
use crate::parser::{helpers, FunctionKind, Parser};
1617
use crate::string::{parse_fstring_literal_element, parse_string_literal, StringType};
@@ -2089,10 +2090,27 @@ impl<'src> Parser<'src> {
20892090
}
20902091

20912092
let value = self.at_expr().then(|| {
2092-
Box::new(
2093-
self.parse_expression_list(ExpressionContext::starred_bitwise_or())
2094-
.expr,
2095-
)
2093+
let parsed_expr = self.parse_expression_list(ExpressionContext::starred_bitwise_or());
2094+
2095+
// test_ok iter_unpack_yield_py37
2096+
// # parse_options: {"target-version": "3.7"}
2097+
// rest = (4, 5, 6)
2098+
// def g(): yield (1, 2, 3, *rest)
2099+
2100+
// test_ok iter_unpack_yield_py38
2101+
// # parse_options: {"target-version": "3.8"}
2102+
// rest = (4, 5, 6)
2103+
// def g(): yield 1, 2, 3, *rest
2104+
// def h(): yield 1, (yield 2, *rest), 3
2105+
2106+
// test_err iter_unpack_yield_py37
2107+
// # parse_options: {"target-version": "3.7"}
2108+
// rest = (4, 5, 6)
2109+
// def g(): yield 1, 2, 3, *rest
2110+
// def h(): yield 1, (yield 2, *rest), 3
2111+
self.check_tuple_unpacking(&parsed_expr, StarTupleKind::Yield);
2112+
2113+
Box::new(parsed_expr.expr)
20962114
});
20972115

20982116
Expr::Yield(ast::ExprYield {

crates/ruff_python_parser/src/parser/statement.rs

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use ruff_python_ast::{
1010
};
1111
use ruff_text_size::{Ranged, TextRange, TextSize};
1212

13+
use crate::error::StarTupleKind;
1314
use crate::parser::expression::{ParsedExpr, EXPR_SET};
1415
use crate::parser::progress::ParserProgress;
1516
use crate::parser::{
@@ -389,10 +390,25 @@ impl<'src> Parser<'src> {
389390
// return x := 1
390391
// return *x and y
391392
let value = self.at_expr().then(|| {
392-
Box::new(
393-
self.parse_expression_list(ExpressionContext::starred_bitwise_or())
394-
.expr,
395-
)
393+
let parsed_expr = self.parse_expression_list(ExpressionContext::starred_bitwise_or());
394+
395+
// test_ok iter_unpack_return_py37
396+
// # parse_options: {"target-version": "3.7"}
397+
// rest = (4, 5, 6)
398+
// def f(): return (1, 2, 3, *rest)
399+
400+
// test_ok iter_unpack_return_py38
401+
// # parse_options: {"target-version": "3.8"}
402+
// rest = (4, 5, 6)
403+
// def f(): return 1, 2, 3, *rest
404+
405+
// test_err iter_unpack_return_py37
406+
// # parse_options: {"target-version": "3.7"}
407+
// rest = (4, 5, 6)
408+
// def f(): return 1, 2, 3, *rest
409+
self.check_tuple_unpacking(&parsed_expr, StarTupleKind::Return);
410+
411+
Box::new(parsed_expr.expr)
396412
});
397413

398414
ast::StmtReturn {
@@ -401,6 +417,33 @@ impl<'src> Parser<'src> {
401417
}
402418
}
403419

420+
/// Report [`UnsupportedSyntaxError`]s for each starred element in `expr` if it is an
421+
/// unparenthesized tuple.
422+
///
423+
/// This method can be used to check for tuple unpacking in return and yield statements, which
424+
/// are only allowed in Python 3.8 and later: <https://github.com/python/cpython/issues/76298>.
425+
pub(crate) fn check_tuple_unpacking(&mut self, expr: &Expr, kind: StarTupleKind) {
426+
let kind = UnsupportedSyntaxErrorKind::StarTuple(kind);
427+
if self.options.target_version >= kind.minimum_version() {
428+
return;
429+
}
430+
431+
let Expr::Tuple(ast::ExprTuple {
432+
elts,
433+
parenthesized: false,
434+
..
435+
}) = expr
436+
else {
437+
return;
438+
};
439+
440+
for elt in elts {
441+
if elt.is_starred_expr() {
442+
self.add_unsupported_syntax_error(kind, elt.range());
443+
}
444+
}
445+
}
446+
404447
/// Parses a `raise` statement.
405448
///
406449
/// # Panics
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
---
2+
source: crates/ruff_python_parser/tests/fixtures.rs
3+
input_file: crates/ruff_python_parser/resources/inline/err/iter_unpack_return_py37.py
4+
---
5+
## AST
6+
7+
```
8+
Module(
9+
ModModule {
10+
range: 0..91,
11+
body: [
12+
Assign(
13+
StmtAssign {
14+
range: 43..59,
15+
targets: [
16+
Name(
17+
ExprName {
18+
range: 43..47,
19+
id: Name("rest"),
20+
ctx: Store,
21+
},
22+
),
23+
],
24+
value: Tuple(
25+
ExprTuple {
26+
range: 50..59,
27+
elts: [
28+
NumberLiteral(
29+
ExprNumberLiteral {
30+
range: 51..52,
31+
value: Int(
32+
4,
33+
),
34+
},
35+
),
36+
NumberLiteral(
37+
ExprNumberLiteral {
38+
range: 54..55,
39+
value: Int(
40+
5,
41+
),
42+
},
43+
),
44+
NumberLiteral(
45+
ExprNumberLiteral {
46+
range: 57..58,
47+
value: Int(
48+
6,
49+
),
50+
},
51+
),
52+
],
53+
ctx: Load,
54+
parenthesized: true,
55+
},
56+
),
57+
},
58+
),
59+
FunctionDef(
60+
StmtFunctionDef {
61+
range: 60..90,
62+
is_async: false,
63+
decorator_list: [],
64+
name: Identifier {
65+
id: Name("f"),
66+
range: 64..65,
67+
},
68+
type_params: None,
69+
parameters: Parameters {
70+
range: 65..67,
71+
posonlyargs: [],
72+
args: [],
73+
vararg: None,
74+
kwonlyargs: [],
75+
kwarg: None,
76+
},
77+
returns: None,
78+
body: [
79+
Return(
80+
StmtReturn {
81+
range: 69..90,
82+
value: Some(
83+
Tuple(
84+
ExprTuple {
85+
range: 76..90,
86+
elts: [
87+
NumberLiteral(
88+
ExprNumberLiteral {
89+
range: 76..77,
90+
value: Int(
91+
1,
92+
),
93+
},
94+
),
95+
NumberLiteral(
96+
ExprNumberLiteral {
97+
range: 79..80,
98+
value: Int(
99+
2,
100+
),
101+
},
102+
),
103+
NumberLiteral(
104+
ExprNumberLiteral {
105+
range: 82..83,
106+
value: Int(
107+
3,
108+
),
109+
},
110+
),
111+
Starred(
112+
ExprStarred {
113+
range: 85..90,
114+
value: Name(
115+
ExprName {
116+
range: 86..90,
117+
id: Name("rest"),
118+
ctx: Load,
119+
},
120+
),
121+
ctx: Load,
122+
},
123+
),
124+
],
125+
ctx: Load,
126+
parenthesized: false,
127+
},
128+
),
129+
),
130+
},
131+
),
132+
],
133+
},
134+
),
135+
],
136+
},
137+
)
138+
```
139+
## Unsupported Syntax Errors
140+
141+
|
142+
1 | # parse_options: {"target-version": "3.7"}
143+
2 | rest = (4, 5, 6)
144+
3 | def f(): return 1, 2, 3, *rest
145+
| ^^^^^ Syntax Error: Cannot use iterable unpacking in return statements on Python 3.7 (syntax was added in Python 3.8)
146+
|

0 commit comments

Comments
 (0)