[syntax-errors] Tuple unpacking in for statement iterator clause before Python 3.9 (#16558)

Summary
--

This PR reuses a slightly modified version of the
`check_tuple_unpacking` method added for detecting unpacking in `return`
and `yield` statements to detect the same issue in the iterator clause
of `for` loops.

I ran into the same issue with a bare `for x in *rest: ...` example
(invalid even on Python 3.13) and added it as a comment on
https://github.com/astral-sh/ruff/issues/16520.

I considered just making this an additional `StarTupleKind` variant as
well, but this change was in a different version of Python, so I kept it
separate.

Test Plan
--

New inline tests.
This commit is contained in:
Brent Westbrook 2025-03-13 15:55:17 -04:00 committed by GitHub
parent 27e9d1fe3e
commit 2382fe1f25
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 673 additions and 6 deletions

View file

@ -616,6 +616,33 @@ pub enum UnsupportedSyntaxErrorKind {
TypeParameterList,
TypeAliasStatement,
TypeParamDefault,
/// Represents the use of tuple unpacking in a `for` statement iterator clause before Python
/// 3.9.
///
/// ## Examples
///
/// Like [`UnsupportedSyntaxErrorKind::StarTuple`] in `return` and `yield` statements, prior to
/// Python 3.9, tuple unpacking in the iterator clause of a `for` statement required
/// parentheses:
///
/// ```python
/// # valid on Python 3.8 and earlier
/// for i in (*a, *b): ...
/// ```
///
/// Omitting the parentheses was invalid:
///
/// ```python
/// for i in *a, *b: ... # SyntaxError
/// ```
///
/// This was changed as part of the [PEG parser rewrite] included in Python 3.9 but not
/// documented directly until the [Python 3.11 release].
///
/// [PEG parser rewrite]: https://peps.python.org/pep-0617/
/// [Python 3.11 release]: https://docs.python.org/3/whatsnew/3.11.html#other-language-changes
UnparenthesizedUnpackInFor,
}
impl Display for UnsupportedSyntaxError {
@ -642,6 +669,9 @@ impl Display for UnsupportedSyntaxError {
UnsupportedSyntaxErrorKind::TypeParamDefault => {
"Cannot set default type for a type parameter"
}
UnsupportedSyntaxErrorKind::UnparenthesizedUnpackInFor => {
"Cannot use iterable unpacking in `for` statements"
}
};
write!(
@ -687,6 +717,9 @@ impl UnsupportedSyntaxErrorKind {
UnsupportedSyntaxErrorKind::TypeParameterList => Change::Added(PythonVersion::PY312),
UnsupportedSyntaxErrorKind::TypeAliasStatement => Change::Added(PythonVersion::PY312),
UnsupportedSyntaxErrorKind::TypeParamDefault => Change::Added(PythonVersion::PY313),
UnsupportedSyntaxErrorKind::UnparenthesizedUnpackInFor => {
Change::Added(PythonVersion::PY39)
}
}
}

View file

@ -2130,7 +2130,10 @@ impl<'src> Parser<'src> {
// rest = (4, 5, 6)
// def g(): yield 1, 2, 3, *rest
// def h(): yield 1, (yield 2, *rest), 3
self.check_tuple_unpacking(&parsed_expr, StarTupleKind::Yield);
self.check_tuple_unpacking(
&parsed_expr,
UnsupportedSyntaxErrorKind::StarTuple(StarTupleKind::Yield),
);
Box::new(parsed_expr.expr)
});

View file

@ -406,7 +406,10 @@ impl<'src> Parser<'src> {
// # parse_options: {"target-version": "3.7"}
// rest = (4, 5, 6)
// def f(): return 1, 2, 3, *rest
self.check_tuple_unpacking(&parsed_expr, StarTupleKind::Return);
self.check_tuple_unpacking(
&parsed_expr,
UnsupportedSyntaxErrorKind::StarTuple(StarTupleKind::Return),
);
Box::new(parsed_expr.expr)
});
@ -420,10 +423,12 @@ impl<'src> Parser<'src> {
/// Report [`UnsupportedSyntaxError`]s for each starred element in `expr` if it is an
/// unparenthesized tuple.
///
/// This method can be used to check for tuple unpacking in return and yield statements, which
/// are only allowed in Python 3.8 and later: <https://github.com/python/cpython/issues/76298>.
pub(crate) fn check_tuple_unpacking(&mut self, expr: &Expr, kind: StarTupleKind) {
let kind = UnsupportedSyntaxErrorKind::StarTuple(kind);
/// This method can be used to check for tuple unpacking in `return`, `yield`, and `for`
/// statements, which are only allowed after [Python 3.8] and [Python 3.9], respectively.
///
/// [Python 3.8]: https://github.com/python/cpython/issues/76298
/// [Python 3.9]: https://github.com/python/cpython/issues/90881
pub(super) fn check_tuple_unpacking(&mut self, expr: &Expr, kind: UnsupportedSyntaxErrorKind) {
if kind.is_supported(self.options.target_version) {
return;
}
@ -1732,6 +1737,28 @@ impl<'src> Parser<'src> {
// for target in x := 1: ...
let iter = self.parse_expression_list(ExpressionContext::starred_bitwise_or());
// test_ok for_iter_unpack_py39
// # parse_options: {"target-version": "3.9"}
// for x in *a, b: ...
// for x in a, *b: ...
// for x in *a, *b: ...
// test_ok for_iter_unpack_py38
// # parse_options: {"target-version": "3.8"}
// for x in (*a, b): ...
// for x in ( a, *b): ...
// for x in (*a, *b): ...
// test_err for_iter_unpack_py38
// # parse_options: {"target-version": "3.8"}
// for x in *a, b: ...
// for x in a, *b: ...
// for x in *a, *b: ...
self.check_tuple_unpacking(
&iter,
UnsupportedSyntaxErrorKind::UnparenthesizedUnpackInFor,
);
self.expect(TokenKind::Colon);
let body = self.parse_body(Clause::For);