Skip to content

Commit 052265d

Browse files
committed
[syntax-errors] Tuple unpacking in return and yield before 3.8
Summary -- Checks for tuple unpacking in `return` and `yield` statements before Python 3.8, as described [here]. I split the `ErrorKind` into two variants for the sake of more precise error messages. I thought that was nicer than having a single `StarTuple` variant containing another enum with two options. Test Plan -- Inline tests. [here]: python/cpython#76298
1 parent d93ed29 commit 052265d

File tree

8 files changed

+752
-4
lines changed

8 files changed

+752
-4
lines changed
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 f(): return 1, 2, 3, *rest
4+
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.7"}
2+
rest = (4, 5, 6)
3+
def f(): return (1, 2, 3, *rest)
4+
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 f(): return 1, 2, 3, *rest
4+
def g(): yield 1, 2, 3, *rest

crates/ruff_python_parser/src/error.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,8 @@ pub enum UnsupportedSyntaxErrorKind {
449449
Match,
450450
Walrus,
451451
ExceptStar,
452+
StarTupleReturn,
453+
StarTupleYield,
452454
}
453455

454456
impl Display for UnsupportedSyntaxError {
@@ -457,6 +459,10 @@ impl Display for UnsupportedSyntaxError {
457459
UnsupportedSyntaxErrorKind::Match => "`match` statement",
458460
UnsupportedSyntaxErrorKind::Walrus => "named assignment expression (`:=`)",
459461
UnsupportedSyntaxErrorKind::ExceptStar => "`except*`",
462+
UnsupportedSyntaxErrorKind::StarTupleReturn => {
463+
"iterable unpacking in return statements"
464+
}
465+
UnsupportedSyntaxErrorKind::StarTupleYield => "iterable unpacking in yield statements",
460466
};
461467
write!(
462468
f,
@@ -474,6 +480,8 @@ impl UnsupportedSyntaxErrorKind {
474480
UnsupportedSyntaxErrorKind::Match => PythonVersion::PY310,
475481
UnsupportedSyntaxErrorKind::Walrus => PythonVersion::PY38,
476482
UnsupportedSyntaxErrorKind::ExceptStar => PythonVersion::PY311,
483+
UnsupportedSyntaxErrorKind::StarTupleReturn
484+
| UnsupportedSyntaxErrorKind::StarTupleYield => PythonVersion::PY38,
477485
}
478486
}
479487
}

crates/ruff_python_parser/src/parser/statement.rs

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,13 @@ impl<'src> Parser<'src> {
294294
let parsed_expr =
295295
self.parse_expression_list(ExpressionContext::yield_or_starred_bitwise_or());
296296

297+
if let Expr::Yield(ast::ExprYield {
298+
value: Some(expr), ..
299+
}) = &parsed_expr.expr
300+
{
301+
self.check_tuple_unpacking(expr);
302+
}
303+
297304
if self.at(TokenKind::Equal) {
298305
Stmt::Assign(self.parse_assign_statement(parsed_expr, start))
299306
} else if self.at(TokenKind::Colon) {
@@ -388,10 +395,29 @@ impl<'src> Parser<'src> {
388395
// return x := 1
389396
// return *x and y
390397
let value = self.at_expr().then(|| {
391-
Box::new(
392-
self.parse_expression_list(ExpressionContext::starred_bitwise_or())
393-
.expr,
394-
)
398+
let parsed_expr = self.parse_expression_list(ExpressionContext::starred_bitwise_or());
399+
400+
// test_ok iter_unpack_return_py37
401+
// # parse_options: {"target-version": "3.7"}
402+
// rest = (4, 5, 6)
403+
// def f(): return (1, 2, 3, *rest)
404+
// def g(): yield (1, 2, 3, *rest)
405+
406+
// test_ok iter_unpack_return_py38
407+
// # parse_options: {"target-version": "3.8"}
408+
// rest = (4, 5, 6)
409+
// def f(): return 1, 2, 3, *rest
410+
// def g(): yield 1, 2, 3, *rest
411+
412+
// test_err iter_unpack_return_py37
413+
// # parse_options: {"target-version": "3.7"}
414+
// rest = (4, 5, 6)
415+
// def f(): return 1, 2, 3, *rest
416+
// def g(): yield 1, 2, 3, *rest
417+
418+
self.check_tuple_unpacking(&parsed_expr);
419+
420+
Box::new(parsed_expr.expr)
395421
});
396422

397423
ast::StmtReturn {
@@ -400,6 +426,34 @@ impl<'src> Parser<'src> {
400426
}
401427
}
402428

429+
/// Report [`UnsupportedSyntaxError`]s for each starred element in `expr` if it is an
430+
/// unparenthesized tuple.
431+
///
432+
/// This method can be used to check for tuple unpacking in return and yield statements, which
433+
/// are only allowed in Python 3.8 and later: <https://github.com/python/cpython/issues/76298>.
434+
fn check_tuple_unpacking(&mut self, expr: &Expr) {
435+
let kind = UnsupportedSyntaxErrorKind::StarTupleReturn;
436+
437+
if self.options.target_version >= kind.minimum_version() {
438+
return;
439+
}
440+
441+
let Expr::Tuple(ast::ExprTuple {
442+
elts,
443+
parenthesized: false,
444+
..
445+
}) = expr
446+
else {
447+
return;
448+
};
449+
450+
for elt in elts {
451+
if elt.is_starred_expr() {
452+
self.add_unsupported_syntax_error(kind, elt.range());
453+
}
454+
}
455+
}
456+
403457
/// Parses a `raise` statement.
404458
///
405459
/// # Panics
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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..121,
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+
FunctionDef(
136+
StmtFunctionDef {
137+
range: 91..120,
138+
is_async: false,
139+
decorator_list: [],
140+
name: Identifier {
141+
id: Name("g"),
142+
range: 95..96,
143+
},
144+
type_params: None,
145+
parameters: Parameters {
146+
range: 96..98,
147+
posonlyargs: [],
148+
args: [],
149+
vararg: None,
150+
kwonlyargs: [],
151+
kwarg: None,
152+
},
153+
returns: None,
154+
body: [
155+
Expr(
156+
StmtExpr {
157+
range: 100..120,
158+
value: Yield(
159+
ExprYield {
160+
range: 100..120,
161+
value: Some(
162+
Tuple(
163+
ExprTuple {
164+
range: 106..120,
165+
elts: [
166+
NumberLiteral(
167+
ExprNumberLiteral {
168+
range: 106..107,
169+
value: Int(
170+
1,
171+
),
172+
},
173+
),
174+
NumberLiteral(
175+
ExprNumberLiteral {
176+
range: 109..110,
177+
value: Int(
178+
2,
179+
),
180+
},
181+
),
182+
NumberLiteral(
183+
ExprNumberLiteral {
184+
range: 112..113,
185+
value: Int(
186+
3,
187+
),
188+
},
189+
),
190+
Starred(
191+
ExprStarred {
192+
range: 115..120,
193+
value: Name(
194+
ExprName {
195+
range: 116..120,
196+
id: Name("rest"),
197+
ctx: Load,
198+
},
199+
),
200+
ctx: Load,
201+
},
202+
),
203+
],
204+
ctx: Load,
205+
parenthesized: false,
206+
},
207+
),
208+
),
209+
},
210+
),
211+
},
212+
),
213+
],
214+
},
215+
),
216+
],
217+
},
218+
)
219+
```
220+
## Unsupported Syntax Errors
221+
222+
|
223+
1 | # parse_options: {"target-version": "3.7"}
224+
2 | rest = (4, 5, 6)
225+
3 | def f(): return 1, 2, 3, *rest
226+
| ^^^^^ Syntax Error: Cannot use iterable unpacking in return statements on Python 3.7 (syntax was added in Python 3.8)
227+
4 | def g(): yield 1, 2, 3, *rest
228+
|
229+
230+
231+
|
232+
2 | rest = (4, 5, 6)
233+
3 | def f(): return 1, 2, 3, *rest
234+
4 | def g(): yield 1, 2, 3, *rest
235+
| ^^^^^ Syntax Error: Cannot use iterable unpacking in return statements on Python 3.7 (syntax was added in Python 3.8)
236+
|

0 commit comments

Comments
 (0)