diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index d664b7da98..d1a013f0af 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -212,13 +212,10 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { if ctx.is_store() { let check_too_many_expressions = checker.is_rule_enabled(Rule::ExpressionsInStarAssignment); - let check_two_starred_expressions = - checker.is_rule_enabled(Rule::MultipleStarredExpressions); pyflakes::rules::starred_expressions( checker, elts, check_too_many_expressions, - check_two_starred_expressions, expr.range(), ); } diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index ed8ab2b585..bdcefb6e19 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -69,8 +69,8 @@ use crate::package::PackageRoot; use crate::preview::is_undefined_export_in_dunder_init_enabled; use crate::registry::Rule; use crate::rules::pyflakes::rules::{ - LateFutureImport, ReturnOutsideFunction, UndefinedLocalWithNestedImportStarUsage, - YieldOutsideFunction, + LateFutureImport, MultipleStarredExpressions, ReturnOutsideFunction, + UndefinedLocalWithNestedImportStarUsage, YieldOutsideFunction, }; use crate::rules::pylint::rules::{ AwaitOutsideAsync, LoadBeforeGlobalDeclaration, YieldFromInAsyncFunction, @@ -685,6 +685,12 @@ impl SemanticSyntaxContext for Checker<'_> { 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::DuplicateTypeParameter | SemanticSyntaxErrorKind::MultipleCaseAssignment(_) diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/starred_expressions.rs b/crates/ruff_linter/src/rules/pyflakes/rules/starred_expressions.rs index b7566b7311..9fe9ec063f 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/starred_expressions.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/starred_expressions.rs @@ -53,27 +53,14 @@ impl Violation for MultipleStarredExpressions { } } -/// F621, F622 +/// F621 pub(crate) fn starred_expressions( checker: &Checker, elts: &[Expr], check_too_many_expressions: bool, - check_two_starred_expressions: bool, location: TextRange, ) { - let mut has_starred: bool = false; - let mut starred_index: Option = 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); - } - } - + let starred_index: Option = None; if check_too_many_expressions { if let Some(starred_index) = starred_index { if starred_index >= 1 << 8 || elts.len() - starred_index > 1 << 24 { diff --git a/crates/ruff_python_parser/resources/inline/err/multiple_starred_assignment_target.py b/crates/ruff_python_parser/resources/inline/err/multiple_starred_assignment_target.py new file mode 100644 index 0000000000..fb25168769 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/multiple_starred_assignment_target.py @@ -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) diff --git a/crates/ruff_python_parser/resources/inline/ok/multiple_starred_assignment_target.py b/crates/ruff_python_parser/resources/inline/ok/multiple_starred_assignment_target.py new file mode 100644 index 0000000000..ae09bbb268 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/multiple_starred_assignment_target.py @@ -0,0 +1,2 @@ +(*a, b) = (1, 2) +(*_, normed), *_ = [(1,), 2] diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index b5d6d65488..b49e31ba5d 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -389,6 +389,40 @@ impl SemanticSyntaxChecker { } } + fn multiple_star_expression( + 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`. fn debug_shadowing(stmt: &ast::Stmt, ctx: &Ctx) { match stmt { @@ -754,6 +788,20 @@ impl SemanticSyntaxChecker { Self::yield_outside_function(ctx, expr, YieldOutsideFunctionKind::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 { parameters: Some(parameters), .. @@ -1035,6 +1083,9 @@ impl Display for SemanticSyntaxError { SemanticSyntaxErrorKind::NonModuleImportStar(name) => { 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 import *` outside module scope. 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)] diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_starred_assignment_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_starred_assignment_target.py.snap new file mode 100644 index 0000000000..01b96d2fc4 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_starred_assignment_target.py.snap @@ -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 + | diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@multiple_starred_assignment_target.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@multiple_starred_assignment_target.py.snap new file mode 100644 index 0000000000..ee5dde4187 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@multiple_starred_assignment_target.py.snap @@ -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, + }, + ), + }, + ), + ], + }, +) +```