[syntax-errors]: multiple-starred-expressions (F622) (#20243)

<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

This PR implements
https://docs.astral.sh/ruff/rules/multiple-starred-expressions/ as a
semantic syntax error

## Test Plan

 I have added inline tests as directed in #17412

---------

Signed-off-by: 11happy <soni5happy@gmail.com>
Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
This commit is contained in:
Bhuminjay Soni 2025-09-25 01:02:55 +05:30 committed by GitHub
parent 73b4b1ed17
commit e6073d0cca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 783 additions and 20 deletions

View file

@ -212,13 +212,10 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
if ctx.is_store() { if ctx.is_store() {
let check_too_many_expressions = let check_too_many_expressions =
checker.is_rule_enabled(Rule::ExpressionsInStarAssignment); checker.is_rule_enabled(Rule::ExpressionsInStarAssignment);
let check_two_starred_expressions =
checker.is_rule_enabled(Rule::MultipleStarredExpressions);
pyflakes::rules::starred_expressions( pyflakes::rules::starred_expressions(
checker, checker,
elts, elts,
check_too_many_expressions, check_too_many_expressions,
check_two_starred_expressions,
expr.range(), expr.range(),
); );
} }

View file

@ -69,8 +69,8 @@ use crate::package::PackageRoot;
use crate::preview::is_undefined_export_in_dunder_init_enabled; use crate::preview::is_undefined_export_in_dunder_init_enabled;
use crate::registry::Rule; use crate::registry::Rule;
use crate::rules::pyflakes::rules::{ use crate::rules::pyflakes::rules::{
LateFutureImport, ReturnOutsideFunction, UndefinedLocalWithNestedImportStarUsage, LateFutureImport, MultipleStarredExpressions, ReturnOutsideFunction,
YieldOutsideFunction, UndefinedLocalWithNestedImportStarUsage, YieldOutsideFunction,
}; };
use crate::rules::pylint::rules::{ use crate::rules::pylint::rules::{
AwaitOutsideAsync, LoadBeforeGlobalDeclaration, YieldFromInAsyncFunction, AwaitOutsideAsync, LoadBeforeGlobalDeclaration, YieldFromInAsyncFunction,
@ -685,6 +685,12 @@ impl SemanticSyntaxContext for Checker<'_> {
self.report_diagnostic(YieldFromInAsyncFunction, error.range); self.report_diagnostic(YieldFromInAsyncFunction, error.range);
} }
} }
SemanticSyntaxErrorKind::MultipleStarredExpressions => {
// F622
if self.is_rule_enabled(Rule::MultipleStarredExpressions) {
self.report_diagnostic(MultipleStarredExpressions, error.range);
}
}
SemanticSyntaxErrorKind::ReboundComprehensionVariable SemanticSyntaxErrorKind::ReboundComprehensionVariable
| SemanticSyntaxErrorKind::DuplicateTypeParameter | SemanticSyntaxErrorKind::DuplicateTypeParameter
| SemanticSyntaxErrorKind::MultipleCaseAssignment(_) | SemanticSyntaxErrorKind::MultipleCaseAssignment(_)

View file

@ -53,27 +53,14 @@ impl Violation for MultipleStarredExpressions {
} }
} }
/// F621, F622 /// F621
pub(crate) fn starred_expressions( pub(crate) fn starred_expressions(
checker: &Checker, checker: &Checker,
elts: &[Expr], elts: &[Expr],
check_too_many_expressions: bool, check_too_many_expressions: bool,
check_two_starred_expressions: bool,
location: TextRange, location: TextRange,
) { ) {
let mut has_starred: bool = false; let starred_index: Option<usize> = None;
let mut starred_index: Option<usize> = None;
for (index, elt) in elts.iter().enumerate() {
if elt.is_starred_expr() {
if has_starred && check_two_starred_expressions {
checker.report_diagnostic(MultipleStarredExpressions, location);
return;
}
has_starred = true;
starred_index = Some(index);
}
}
if check_too_many_expressions { if check_too_many_expressions {
if let Some(starred_index) = starred_index { if let Some(starred_index) = starred_index {
if starred_index >= 1 << 8 || elts.len() - starred_index > 1 << 24 { if starred_index >= 1 << 8 || elts.len() - starred_index > 1 << 24 {

View file

@ -0,0 +1,5 @@
(*a, *b) = (1, 2)
[*a, *b] = (1, 2)
(*a, *b, c) = (1, 2, 3)
[*a, *b, c] = (1, 2, 3)
(*a, *b, (*c, *d)) = (1, 2)

View file

@ -0,0 +1,2 @@
(*a, b) = (1, 2)
(*_, normed), *_ = [(1,), 2]

View file

@ -389,6 +389,40 @@ impl SemanticSyntaxChecker {
} }
} }
fn multiple_star_expression<Ctx: SemanticSyntaxContext>(
ctx: &Ctx,
expr_ctx: ExprContext,
elts: &[Expr],
range: TextRange,
) {
if expr_ctx.is_store() {
let mut has_starred = false;
for elt in elts {
if elt.is_starred_expr() {
if has_starred {
// test_err multiple_starred_assignment_target
// (*a, *b) = (1, 2)
// [*a, *b] = (1, 2)
// (*a, *b, c) = (1, 2, 3)
// [*a, *b, c] = (1, 2, 3)
// (*a, *b, (*c, *d)) = (1, 2)
// test_ok multiple_starred_assignment_target
// (*a, b) = (1, 2)
// (*_, normed), *_ = [(1,), 2]
Self::add_error(
ctx,
SemanticSyntaxErrorKind::MultipleStarredExpressions,
range,
);
return;
}
has_starred = true;
}
}
}
}
/// Check for [`SemanticSyntaxErrorKind::WriteToDebug`] in `stmt`. /// Check for [`SemanticSyntaxErrorKind::WriteToDebug`] in `stmt`.
fn debug_shadowing<Ctx: SemanticSyntaxContext>(stmt: &ast::Stmt, ctx: &Ctx) { fn debug_shadowing<Ctx: SemanticSyntaxContext>(stmt: &ast::Stmt, ctx: &Ctx) {
match stmt { match stmt {
@ -754,6 +788,20 @@ impl SemanticSyntaxChecker {
Self::yield_outside_function(ctx, expr, YieldOutsideFunctionKind::Await); Self::yield_outside_function(ctx, expr, YieldOutsideFunctionKind::Await);
Self::await_outside_async_function(ctx, expr, AwaitOutsideAsyncFunctionKind::Await); Self::await_outside_async_function(ctx, expr, AwaitOutsideAsyncFunctionKind::Await);
} }
Expr::Tuple(ast::ExprTuple {
elts,
ctx: expr_ctx,
range,
..
})
| Expr::List(ast::ExprList {
elts,
ctx: expr_ctx,
range,
..
}) => {
Self::multiple_star_expression(ctx, *expr_ctx, elts, *range);
}
Expr::Lambda(ast::ExprLambda { Expr::Lambda(ast::ExprLambda {
parameters: Some(parameters), parameters: Some(parameters),
.. ..
@ -1035,6 +1083,9 @@ impl Display for SemanticSyntaxError {
SemanticSyntaxErrorKind::NonModuleImportStar(name) => { SemanticSyntaxErrorKind::NonModuleImportStar(name) => {
write!(f, "`from {name} import *` only allowed at module level") write!(f, "`from {name} import *` only allowed at module level")
} }
SemanticSyntaxErrorKind::MultipleStarredExpressions => {
write!(f, "Two starred expressions in assignment")
}
} }
} }
} }
@ -1398,6 +1449,13 @@ pub enum SemanticSyntaxErrorKind {
/// Represents the use of `from <module> import *` outside module scope. /// Represents the use of `from <module> import *` outside module scope.
NonModuleImportStar(String), NonModuleImportStar(String),
/// Represents the use of more than one starred expression in an assignment.
///
/// Python only allows a single starred target when unpacking values on the
/// left-hand side of an assignment. Using multiple starred expressions makes
/// the statement invalid and results in a `SyntaxError`.
MultipleStarredExpressions,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)]

View file

@ -0,0 +1,520 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/multiple_starred_assignment_target.py
---
## AST
```
Module(
ModModule {
node_index: NodeIndex(None),
range: 0..112,
body: [
Assign(
StmtAssign {
node_index: NodeIndex(None),
range: 0..17,
targets: [
Tuple(
ExprTuple {
node_index: NodeIndex(None),
range: 0..8,
elts: [
Starred(
ExprStarred {
node_index: NodeIndex(None),
range: 1..3,
value: Name(
ExprName {
node_index: NodeIndex(None),
range: 2..3,
id: Name("a"),
ctx: Store,
},
),
ctx: Store,
},
),
Starred(
ExprStarred {
node_index: NodeIndex(None),
range: 5..7,
value: Name(
ExprName {
node_index: NodeIndex(None),
range: 6..7,
id: Name("b"),
ctx: Store,
},
),
ctx: Store,
},
),
],
ctx: Store,
parenthesized: true,
},
),
],
value: Tuple(
ExprTuple {
node_index: NodeIndex(None),
range: 11..17,
elts: [
NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 12..13,
value: Int(
1,
),
},
),
NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 15..16,
value: Int(
2,
),
},
),
],
ctx: Load,
parenthesized: true,
},
),
},
),
Assign(
StmtAssign {
node_index: NodeIndex(None),
range: 18..35,
targets: [
List(
ExprList {
node_index: NodeIndex(None),
range: 18..26,
elts: [
Starred(
ExprStarred {
node_index: NodeIndex(None),
range: 19..21,
value: Name(
ExprName {
node_index: NodeIndex(None),
range: 20..21,
id: Name("a"),
ctx: Store,
},
),
ctx: Store,
},
),
Starred(
ExprStarred {
node_index: NodeIndex(None),
range: 23..25,
value: Name(
ExprName {
node_index: NodeIndex(None),
range: 24..25,
id: Name("b"),
ctx: Store,
},
),
ctx: Store,
},
),
],
ctx: Store,
},
),
],
value: Tuple(
ExprTuple {
node_index: NodeIndex(None),
range: 29..35,
elts: [
NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 30..31,
value: Int(
1,
),
},
),
NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 33..34,
value: Int(
2,
),
},
),
],
ctx: Load,
parenthesized: true,
},
),
},
),
Assign(
StmtAssign {
node_index: NodeIndex(None),
range: 36..59,
targets: [
Tuple(
ExprTuple {
node_index: NodeIndex(None),
range: 36..47,
elts: [
Starred(
ExprStarred {
node_index: NodeIndex(None),
range: 37..39,
value: Name(
ExprName {
node_index: NodeIndex(None),
range: 38..39,
id: Name("a"),
ctx: Store,
},
),
ctx: Store,
},
),
Starred(
ExprStarred {
node_index: NodeIndex(None),
range: 41..43,
value: Name(
ExprName {
node_index: NodeIndex(None),
range: 42..43,
id: Name("b"),
ctx: Store,
},
),
ctx: Store,
},
),
Name(
ExprName {
node_index: NodeIndex(None),
range: 45..46,
id: Name("c"),
ctx: Store,
},
),
],
ctx: Store,
parenthesized: true,
},
),
],
value: Tuple(
ExprTuple {
node_index: NodeIndex(None),
range: 50..59,
elts: [
NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 51..52,
value: Int(
1,
),
},
),
NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 54..55,
value: Int(
2,
),
},
),
NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 57..58,
value: Int(
3,
),
},
),
],
ctx: Load,
parenthesized: true,
},
),
},
),
Assign(
StmtAssign {
node_index: NodeIndex(None),
range: 60..83,
targets: [
List(
ExprList {
node_index: NodeIndex(None),
range: 60..71,
elts: [
Starred(
ExprStarred {
node_index: NodeIndex(None),
range: 61..63,
value: Name(
ExprName {
node_index: NodeIndex(None),
range: 62..63,
id: Name("a"),
ctx: Store,
},
),
ctx: Store,
},
),
Starred(
ExprStarred {
node_index: NodeIndex(None),
range: 65..67,
value: Name(
ExprName {
node_index: NodeIndex(None),
range: 66..67,
id: Name("b"),
ctx: Store,
},
),
ctx: Store,
},
),
Name(
ExprName {
node_index: NodeIndex(None),
range: 69..70,
id: Name("c"),
ctx: Store,
},
),
],
ctx: Store,
},
),
],
value: Tuple(
ExprTuple {
node_index: NodeIndex(None),
range: 74..83,
elts: [
NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 75..76,
value: Int(
1,
),
},
),
NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 78..79,
value: Int(
2,
),
},
),
NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 81..82,
value: Int(
3,
),
},
),
],
ctx: Load,
parenthesized: true,
},
),
},
),
Assign(
StmtAssign {
node_index: NodeIndex(None),
range: 84..111,
targets: [
Tuple(
ExprTuple {
node_index: NodeIndex(None),
range: 84..102,
elts: [
Starred(
ExprStarred {
node_index: NodeIndex(None),
range: 85..87,
value: Name(
ExprName {
node_index: NodeIndex(None),
range: 86..87,
id: Name("a"),
ctx: Store,
},
),
ctx: Store,
},
),
Starred(
ExprStarred {
node_index: NodeIndex(None),
range: 89..91,
value: Name(
ExprName {
node_index: NodeIndex(None),
range: 90..91,
id: Name("b"),
ctx: Store,
},
),
ctx: Store,
},
),
Tuple(
ExprTuple {
node_index: NodeIndex(None),
range: 93..101,
elts: [
Starred(
ExprStarred {
node_index: NodeIndex(None),
range: 94..96,
value: Name(
ExprName {
node_index: NodeIndex(None),
range: 95..96,
id: Name("c"),
ctx: Store,
},
),
ctx: Store,
},
),
Starred(
ExprStarred {
node_index: NodeIndex(None),
range: 98..100,
value: Name(
ExprName {
node_index: NodeIndex(None),
range: 99..100,
id: Name("d"),
ctx: Store,
},
),
ctx: Store,
},
),
],
ctx: Store,
parenthesized: true,
},
),
],
ctx: Store,
parenthesized: true,
},
),
],
value: Tuple(
ExprTuple {
node_index: NodeIndex(None),
range: 105..111,
elts: [
NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 106..107,
value: Int(
1,
),
},
),
NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 109..110,
value: Int(
2,
),
},
),
],
ctx: Load,
parenthesized: true,
},
),
},
),
],
},
)
```
## Semantic Syntax Errors
|
1 | (*a, *b) = (1, 2)
| ^^^^^^^^ Syntax Error: Two starred expressions in assignment
2 | [*a, *b] = (1, 2)
3 | (*a, *b, c) = (1, 2, 3)
|
|
1 | (*a, *b) = (1, 2)
2 | [*a, *b] = (1, 2)
| ^^^^^^^^ Syntax Error: Two starred expressions in assignment
3 | (*a, *b, c) = (1, 2, 3)
4 | [*a, *b, c] = (1, 2, 3)
|
|
1 | (*a, *b) = (1, 2)
2 | [*a, *b] = (1, 2)
3 | (*a, *b, c) = (1, 2, 3)
| ^^^^^^^^^^^ Syntax Error: Two starred expressions in assignment
4 | [*a, *b, c] = (1, 2, 3)
5 | (*a, *b, (*c, *d)) = (1, 2)
|
|
2 | [*a, *b] = (1, 2)
3 | (*a, *b, c) = (1, 2, 3)
4 | [*a, *b, c] = (1, 2, 3)
| ^^^^^^^^^^^ Syntax Error: Two starred expressions in assignment
5 | (*a, *b, (*c, *d)) = (1, 2)
|
|
3 | (*a, *b, c) = (1, 2, 3)
4 | [*a, *b, c] = (1, 2, 3)
5 | (*a, *b, (*c, *d)) = (1, 2)
| ^^^^^^^^^^^^^^^^^^ Syntax Error: Two starred expressions in assignment
|
|
3 | (*a, *b, c) = (1, 2, 3)
4 | [*a, *b, c] = (1, 2, 3)
5 | (*a, *b, (*c, *d)) = (1, 2)
| ^^^^^^^^ Syntax Error: Two starred expressions in assignment
|

View file

@ -0,0 +1,188 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/ok/multiple_starred_assignment_target.py
---
## AST
```
Module(
ModModule {
node_index: NodeIndex(None),
range: 0..46,
body: [
Assign(
StmtAssign {
node_index: NodeIndex(None),
range: 0..16,
targets: [
Tuple(
ExprTuple {
node_index: NodeIndex(None),
range: 0..7,
elts: [
Starred(
ExprStarred {
node_index: NodeIndex(None),
range: 1..3,
value: Name(
ExprName {
node_index: NodeIndex(None),
range: 2..3,
id: Name("a"),
ctx: Store,
},
),
ctx: Store,
},
),
Name(
ExprName {
node_index: NodeIndex(None),
range: 5..6,
id: Name("b"),
ctx: Store,
},
),
],
ctx: Store,
parenthesized: true,
},
),
],
value: Tuple(
ExprTuple {
node_index: NodeIndex(None),
range: 10..16,
elts: [
NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 11..12,
value: Int(
1,
),
},
),
NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 14..15,
value: Int(
2,
),
},
),
],
ctx: Load,
parenthesized: true,
},
),
},
),
Assign(
StmtAssign {
node_index: NodeIndex(None),
range: 17..45,
targets: [
Tuple(
ExprTuple {
node_index: NodeIndex(None),
range: 17..33,
elts: [
Tuple(
ExprTuple {
node_index: NodeIndex(None),
range: 17..29,
elts: [
Starred(
ExprStarred {
node_index: NodeIndex(None),
range: 18..20,
value: Name(
ExprName {
node_index: NodeIndex(None),
range: 19..20,
id: Name("_"),
ctx: Store,
},
),
ctx: Store,
},
),
Name(
ExprName {
node_index: NodeIndex(None),
range: 22..28,
id: Name("normed"),
ctx: Store,
},
),
],
ctx: Store,
parenthesized: true,
},
),
Starred(
ExprStarred {
node_index: NodeIndex(None),
range: 31..33,
value: Name(
ExprName {
node_index: NodeIndex(None),
range: 32..33,
id: Name("_"),
ctx: Store,
},
),
ctx: Store,
},
),
],
ctx: Store,
parenthesized: false,
},
),
],
value: List(
ExprList {
node_index: NodeIndex(None),
range: 36..45,
elts: [
Tuple(
ExprTuple {
node_index: NodeIndex(None),
range: 37..41,
elts: [
NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 38..39,
value: Int(
1,
),
},
),
],
ctx: Load,
parenthesized: true,
},
),
NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 43..44,
value: Int(
2,
),
},
),
],
ctx: Load,
},
),
},
),
],
},
)
```