From 9dff5bf7e415b4e135e07afa9338c59849a2b5d6 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 24 Mar 2025 12:20:11 -0400 Subject: [PATCH 1/7] [syntax-errors] Multiple assignments in `case` pattern Summary -- This PR detects multiple assignments to the same name in `case` patterns by recursively visiting each pattern. Test Plan -- New inline tests. --- crates/ruff_linter/src/checkers/ast/mod.rs | 4 +- .../multiple_assignment_in_case_pattern.py | 10 + .../ok/multiple_assignment_in_case_pattern.py | 2 + .../ruff_python_parser/src/semantic_errors.rs | 125 ++- ...ultiple_assignment_in_case_pattern.py.snap | 723 ++++++++++++++++++ ...ultiple_assignment_in_case_pattern.py.snap | 115 +++ 6 files changed, 977 insertions(+), 2 deletions(-) create mode 100644 crates/ruff_python_parser/resources/inline/err/multiple_assignment_in_case_pattern.py create mode 100644 crates/ruff_python_parser/resources/inline/ok/multiple_assignment_in_case_pattern.py create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_case_pattern.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/valid_syntax@multiple_assignment_in_case_pattern.py.snap diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 0c9864c6bf80f..858ea7c74e965 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -549,12 +549,14 @@ impl SemanticSyntaxContext for Checker<'_> { } SemanticSyntaxErrorKind::ReboundComprehensionVariable | SemanticSyntaxErrorKind::DuplicateTypeParameter + | SemanticSyntaxErrorKind::MultipleCaseAssignment if self.settings.preview.is_enabled() => { self.semantic_errors.borrow_mut().push(error); } SemanticSyntaxErrorKind::ReboundComprehensionVariable - | SemanticSyntaxErrorKind::DuplicateTypeParameter => {} + | SemanticSyntaxErrorKind::DuplicateTypeParameter + | SemanticSyntaxErrorKind::MultipleCaseAssignment => {} } } } diff --git a/crates/ruff_python_parser/resources/inline/err/multiple_assignment_in_case_pattern.py b/crates/ruff_python_parser/resources/inline/err/multiple_assignment_in_case_pattern.py new file mode 100644 index 0000000000000..f9fff6fcca1c1 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/multiple_assignment_in_case_pattern.py @@ -0,0 +1,10 @@ +match 2: + case x as x: ... # MatchAs + case [y, z, y]: ... # MatchSequence + case [y, z, *y]: ... # MatchSequence + case [y, y, y]: ... # MatchSequence multiple + case {1: x, 2: x}: ... # MatchMapping duplicate pattern + case {1: x, **x}: ... # MatchMapping duplicate in **rest + case Class(x, x): ... # MatchClass positional + case Class(x=1, x=2): ... # MatchClass keyword + case [x] | {1: x} | Class(x=1, x=2): ... # MatchOr diff --git a/crates/ruff_python_parser/resources/inline/ok/multiple_assignment_in_case_pattern.py b/crates/ruff_python_parser/resources/inline/ok/multiple_assignment_in_case_pattern.py new file mode 100644 index 0000000000000..05b1908b246d6 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/multiple_assignment_in_case_pattern.py @@ -0,0 +1,2 @@ +match 2: + case Class(x) | [x] | x: ... diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index de03216ea10e6..451bb9994e0f6 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -9,7 +9,7 @@ use std::fmt::Display; use ruff_python_ast::{ self as ast, visitor::{walk_expr, Visitor}, - Expr, PythonVersion, Stmt, StmtExpr, StmtImportFrom, + Expr, Pattern, PythonVersion, Stmt, StmtExpr, StmtImportFrom, }; use ruff_text_size::{Ranged, TextRange}; @@ -61,6 +61,7 @@ impl SemanticSyntaxChecker { } Self::duplicate_type_parameter_name(stmt, ctx); + Self::multiple_case_assignment(stmt, ctx); } fn duplicate_type_parameter_name(stmt: &ast::Stmt, ctx: &Ctx) { @@ -109,6 +110,20 @@ impl SemanticSyntaxChecker { } } + fn multiple_case_assignment(stmt: &Stmt, ctx: &Ctx) { + let Stmt::Match(ast::StmtMatch { cases, .. }) = stmt else { + return; + }; + + for case in cases { + let mut visitor = MultipleCaseAssignmentVisitor { + names: Vec::new(), + ctx, + }; + visitor.visit_pattern(&case.pattern); + } + } + pub fn visit_stmt(&mut self, stmt: &ast::Stmt, ctx: &Ctx) { // update internal state match stmt { @@ -219,6 +234,9 @@ impl Display for SemanticSyntaxError { SemanticSyntaxErrorKind::DuplicateTypeParameter => { f.write_str("duplicate type parameter") } + SemanticSyntaxErrorKind::MultipleCaseAssignment => { + f.write_str("multiple assignments in pattern") + } } } } @@ -265,6 +283,18 @@ pub enum SemanticSyntaxErrorKind { /// class C[T, T]: ... /// ``` DuplicateTypeParameter, + + /// Represents a duplicate binding in a `case` pattern of a `match` statement. + /// + /// ## Examples + /// + /// ```python + /// match x: + /// case [x, y, x]: ... + /// case x as x: ... + /// case Class(x=1, x=2): ... + /// ``` + MultipleCaseAssignment, } /// Searches for the first named expression (`x := y`) rebinding one of the `iteration_variables` in @@ -291,6 +321,99 @@ impl Visitor<'_> for ReboundComprehensionVisitor<'_> { } } +struct MultipleCaseAssignmentVisitor<'a, Ctx> { + names: Vec<&'a ast::Identifier>, + ctx: &'a Ctx, +} + +impl<'a, Ctx: SemanticSyntaxContext> MultipleCaseAssignmentVisitor<'a, Ctx> { + /// Check if `other` is present in `self.names` and emit an error if so. + fn push(&mut self, other: &'a ast::Identifier) { + for ident in &self.names { + if other.id == ident.id { + let range = if ident.range.start() > other.range.start() { + ident.range + } else { + other.range + }; + SemanticSyntaxChecker::add_error( + self.ctx, + SemanticSyntaxErrorKind::MultipleCaseAssignment, + range, + ); + return; + } + } + self.names.push(other); + } + + fn visit_pattern(&mut self, pattern: &'a Pattern) { + // test_err multiple_assignment_in_case_pattern + // match 2: + // case x as x: ... # MatchAs + // case [y, z, y]: ... # MatchSequence + // case [y, z, *y]: ... # MatchSequence + // case [y, y, y]: ... # MatchSequence multiple + // case {1: x, 2: x}: ... # MatchMapping duplicate pattern + // case {1: x, **x}: ... # MatchMapping duplicate in **rest + // case Class(x, x): ... # MatchClass positional + // case Class(x=1, x=2): ... # MatchClass keyword + // case [x] | {1: x} | Class(x=1, x=2): ... # MatchOr + + // test_ok multiple_assignment_in_case_pattern + // match 2: + // case Class(x) | [x] | x: ... + match pattern { + Pattern::MatchValue(_) | Pattern::MatchSingleton(_) => {} + Pattern::MatchStar(ast::PatternMatchStar { name, .. }) => { + if let Some(name) = name { + self.push(name); + } + } + Pattern::MatchSequence(ast::PatternMatchSequence { patterns, .. }) => { + for pattern in patterns { + self.visit_pattern(pattern); + } + } + Pattern::MatchMapping(ast::PatternMatchMapping { patterns, rest, .. }) => { + for pattern in patterns { + self.visit_pattern(pattern); + } + if let Some(rest) = rest { + self.push(rest); + } + } + Pattern::MatchClass(ast::PatternMatchClass { arguments, .. }) => { + for pattern in &arguments.patterns { + self.visit_pattern(pattern); + } + for keyword in &arguments.keywords { + self.push(&keyword.attr); + self.visit_pattern(&keyword.pattern); + } + } + Pattern::MatchAs(ast::PatternMatchAs { pattern, name, .. }) => { + if let Some(name) = name { + self.push(name); + } + if let Some(pattern) = pattern { + self.visit_pattern(pattern); + } + } + Pattern::MatchOr(ast::PatternMatchOr { patterns, .. }) => { + // each of these patterns should be visited separately + for pattern in patterns { + let mut visitor = Self { + names: Vec::new(), + ctx: self.ctx, + }; + visitor.visit_pattern(pattern); + } + } + } + } +} + pub trait SemanticSyntaxContext { /// Returns `true` if a module's docstring boundary has been passed. fn seen_docstring_boundary(&self) -> bool; diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_case_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_case_pattern.py.snap new file mode 100644 index 0000000000000..75694ff9344d1 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_case_pattern.py.snap @@ -0,0 +1,723 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/multiple_assignment_in_case_pattern.py +snapshot_kind: text +--- +## AST + +``` +Module( + ModModule { + range: 0..456, + body: [ + Match( + StmtMatch { + range: 0..444, + subject: NumberLiteral( + ExprNumberLiteral { + range: 6..7, + value: Int( + 2, + ), + }, + ), + cases: [ + MatchCase { + range: 13..29, + pattern: MatchAs( + PatternMatchAs { + range: 18..24, + pattern: Some( + MatchAs( + PatternMatchAs { + range: 18..19, + pattern: None, + name: Some( + Identifier { + id: Name("x"), + range: 18..19, + }, + ), + }, + ), + ), + name: Some( + Identifier { + id: Name("x"), + range: 23..24, + }, + ), + }, + ), + guard: None, + body: [ + Expr( + StmtExpr { + range: 26..29, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 26..29, + }, + ), + }, + ), + ], + }, + MatchCase { + range: 45..64, + pattern: MatchSequence( + PatternMatchSequence { + range: 50..59, + patterns: [ + MatchAs( + PatternMatchAs { + range: 51..52, + pattern: None, + name: Some( + Identifier { + id: Name("y"), + range: 51..52, + }, + ), + }, + ), + MatchAs( + PatternMatchAs { + range: 54..55, + pattern: None, + name: Some( + Identifier { + id: Name("z"), + range: 54..55, + }, + ), + }, + ), + MatchAs( + PatternMatchAs { + range: 57..58, + pattern: None, + name: Some( + Identifier { + id: Name("y"), + range: 57..58, + }, + ), + }, + ), + ], + }, + ), + guard: None, + body: [ + Expr( + StmtExpr { + range: 61..64, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 61..64, + }, + ), + }, + ), + ], + }, + MatchCase { + range: 86..106, + pattern: MatchSequence( + PatternMatchSequence { + range: 91..101, + patterns: [ + MatchAs( + PatternMatchAs { + range: 92..93, + pattern: None, + name: Some( + Identifier { + id: Name("y"), + range: 92..93, + }, + ), + }, + ), + MatchAs( + PatternMatchAs { + range: 95..96, + pattern: None, + name: Some( + Identifier { + id: Name("z"), + range: 95..96, + }, + ), + }, + ), + MatchStar( + PatternMatchStar { + range: 98..100, + name: Some( + Identifier { + id: Name("y"), + range: 99..100, + }, + ), + }, + ), + ], + }, + ), + guard: None, + body: [ + Expr( + StmtExpr { + range: 103..106, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 103..106, + }, + ), + }, + ), + ], + }, + MatchCase { + range: 128..147, + pattern: MatchSequence( + PatternMatchSequence { + range: 133..142, + patterns: [ + MatchAs( + PatternMatchAs { + range: 134..135, + pattern: None, + name: Some( + Identifier { + id: Name("y"), + range: 134..135, + }, + ), + }, + ), + MatchAs( + PatternMatchAs { + range: 137..138, + pattern: None, + name: Some( + Identifier { + id: Name("y"), + range: 137..138, + }, + ), + }, + ), + MatchAs( + PatternMatchAs { + range: 140..141, + pattern: None, + name: Some( + Identifier { + id: Name("y"), + range: 140..141, + }, + ), + }, + ), + ], + }, + ), + guard: None, + body: [ + Expr( + StmtExpr { + range: 144..147, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 144..147, + }, + ), + }, + ), + ], + }, + MatchCase { + range: 178..200, + pattern: MatchMapping( + PatternMatchMapping { + range: 183..195, + keys: [ + NumberLiteral( + ExprNumberLiteral { + range: 184..185, + value: Int( + 1, + ), + }, + ), + NumberLiteral( + ExprNumberLiteral { + range: 190..191, + value: Int( + 2, + ), + }, + ), + ], + patterns: [ + MatchAs( + PatternMatchAs { + range: 187..188, + pattern: None, + name: Some( + Identifier { + id: Name("x"), + range: 187..188, + }, + ), + }, + ), + MatchAs( + PatternMatchAs { + range: 193..194, + pattern: None, + name: Some( + Identifier { + id: Name("x"), + range: 193..194, + }, + ), + }, + ), + ], + rest: None, + }, + ), + guard: None, + body: [ + Expr( + StmtExpr { + range: 197..200, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 197..200, + }, + ), + }, + ), + ], + }, + MatchCase { + range: 239..260, + pattern: MatchMapping( + PatternMatchMapping { + range: 244..255, + keys: [ + NumberLiteral( + ExprNumberLiteral { + range: 245..246, + value: Int( + 1, + ), + }, + ), + ], + patterns: [ + MatchAs( + PatternMatchAs { + range: 248..249, + pattern: None, + name: Some( + Identifier { + id: Name("x"), + range: 248..249, + }, + ), + }, + ), + ], + rest: Some( + Identifier { + id: Name("x"), + range: 253..254, + }, + ), + }, + ), + guard: None, + body: [ + Expr( + StmtExpr { + range: 257..260, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 257..260, + }, + ), + }, + ), + ], + }, + MatchCase { + range: 301..322, + pattern: MatchClass( + PatternMatchClass { + range: 306..317, + cls: Name( + ExprName { + range: 306..311, + id: Name("Class"), + ctx: Load, + }, + ), + arguments: PatternArguments { + range: 311..317, + patterns: [ + MatchAs( + PatternMatchAs { + range: 312..313, + pattern: None, + name: Some( + Identifier { + id: Name("x"), + range: 312..313, + }, + ), + }, + ), + MatchAs( + PatternMatchAs { + range: 315..316, + pattern: None, + name: Some( + Identifier { + id: Name("x"), + range: 315..316, + }, + ), + }, + ), + ], + keywords: [], + }, + }, + ), + guard: None, + body: [ + Expr( + StmtExpr { + range: 319..322, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 319..322, + }, + ), + }, + ), + ], + }, + MatchCase { + range: 352..377, + pattern: MatchClass( + PatternMatchClass { + range: 357..372, + cls: Name( + ExprName { + range: 357..362, + id: Name("Class"), + ctx: Load, + }, + ), + arguments: PatternArguments { + range: 362..372, + patterns: [], + keywords: [ + PatternKeyword { + range: 363..366, + attr: Identifier { + id: Name("x"), + range: 363..364, + }, + pattern: MatchValue( + PatternMatchValue { + range: 365..366, + value: NumberLiteral( + ExprNumberLiteral { + range: 365..366, + value: Int( + 1, + ), + }, + ), + }, + ), + }, + PatternKeyword { + range: 368..371, + attr: Identifier { + id: Name("x"), + range: 368..369, + }, + pattern: MatchValue( + PatternMatchValue { + range: 370..371, + value: NumberLiteral( + ExprNumberLiteral { + range: 370..371, + value: Int( + 2, + ), + }, + ), + }, + ), + }, + ], + }, + }, + ), + guard: None, + body: [ + Expr( + StmtExpr { + range: 374..377, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 374..377, + }, + ), + }, + ), + ], + }, + MatchCase { + range: 404..444, + pattern: MatchOr( + PatternMatchOr { + range: 409..439, + patterns: [ + MatchSequence( + PatternMatchSequence { + range: 409..412, + patterns: [ + MatchAs( + PatternMatchAs { + range: 410..411, + pattern: None, + name: Some( + Identifier { + id: Name("x"), + range: 410..411, + }, + ), + }, + ), + ], + }, + ), + MatchMapping( + PatternMatchMapping { + range: 415..421, + keys: [ + NumberLiteral( + ExprNumberLiteral { + range: 416..417, + value: Int( + 1, + ), + }, + ), + ], + patterns: [ + MatchAs( + PatternMatchAs { + range: 419..420, + pattern: None, + name: Some( + Identifier { + id: Name("x"), + range: 419..420, + }, + ), + }, + ), + ], + rest: None, + }, + ), + MatchClass( + PatternMatchClass { + range: 424..439, + cls: Name( + ExprName { + range: 424..429, + id: Name("Class"), + ctx: Load, + }, + ), + arguments: PatternArguments { + range: 429..439, + patterns: [], + keywords: [ + PatternKeyword { + range: 430..433, + attr: Identifier { + id: Name("x"), + range: 430..431, + }, + pattern: MatchValue( + PatternMatchValue { + range: 432..433, + value: NumberLiteral( + ExprNumberLiteral { + range: 432..433, + value: Int( + 1, + ), + }, + ), + }, + ), + }, + PatternKeyword { + range: 435..438, + attr: Identifier { + id: Name("x"), + range: 435..436, + }, + pattern: MatchValue( + PatternMatchValue { + range: 437..438, + value: NumberLiteral( + ExprNumberLiteral { + range: 437..438, + value: Int( + 2, + ), + }, + ), + }, + ), + }, + ], + }, + }, + ), + ], + }, + ), + guard: None, + body: [ + Expr( + StmtExpr { + range: 441..444, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 441..444, + }, + ), + }, + ), + ], + }, + ], + }, + ), + ], + }, +) +``` +## Semantic Syntax Errors + + | +1 | match 2: +2 | case x as x: ... # MatchAs + | ^ Syntax Error: multiple assignments in pattern +3 | case [y, z, y]: ... # MatchSequence +4 | case [y, z, *y]: ... # MatchSequence + | + + + | +1 | match 2: +2 | case x as x: ... # MatchAs +3 | case [y, z, y]: ... # MatchSequence + | ^ Syntax Error: multiple assignments in pattern +4 | case [y, z, *y]: ... # MatchSequence +5 | case [y, y, y]: ... # MatchSequence multiple + | + + + | +2 | case x as x: ... # MatchAs +3 | case [y, z, y]: ... # MatchSequence +4 | case [y, z, *y]: ... # MatchSequence + | ^ Syntax Error: multiple assignments in pattern +5 | case [y, y, y]: ... # MatchSequence multiple +6 | case {1: x, 2: x}: ... # MatchMapping duplicate pattern + | + + + | +3 | case [y, z, y]: ... # MatchSequence +4 | case [y, z, *y]: ... # MatchSequence +5 | case [y, y, y]: ... # MatchSequence multiple + | ^ Syntax Error: multiple assignments in pattern +6 | case {1: x, 2: x}: ... # MatchMapping duplicate pattern +7 | case {1: x, **x}: ... # MatchMapping duplicate in **rest + | + + + | +3 | case [y, z, y]: ... # MatchSequence +4 | case [y, z, *y]: ... # MatchSequence +5 | case [y, y, y]: ... # MatchSequence multiple + | ^ Syntax Error: multiple assignments in pattern +6 | case {1: x, 2: x}: ... # MatchMapping duplicate pattern +7 | case {1: x, **x}: ... # MatchMapping duplicate in **rest + | + + + | +4 | case [y, z, *y]: ... # MatchSequence +5 | case [y, y, y]: ... # MatchSequence multiple +6 | case {1: x, 2: x}: ... # MatchMapping duplicate pattern + | ^ Syntax Error: multiple assignments in pattern +7 | case {1: x, **x}: ... # MatchMapping duplicate in **rest +8 | case Class(x, x): ... # MatchClass positional + | + + + | +5 | case [y, y, y]: ... # MatchSequence multiple +6 | case {1: x, 2: x}: ... # MatchMapping duplicate pattern +7 | case {1: x, **x}: ... # MatchMapping duplicate in **rest + | ^ Syntax Error: multiple assignments in pattern +8 | case Class(x, x): ... # MatchClass positional +9 | case Class(x=1, x=2): ... # MatchClass keyword + | + + + | + 6 | case {1: x, 2: x}: ... # MatchMapping duplicate pattern + 7 | case {1: x, **x}: ... # MatchMapping duplicate in **rest + 8 | case Class(x, x): ... # MatchClass positional + | ^ Syntax Error: multiple assignments in pattern + 9 | case Class(x=1, x=2): ... # MatchClass keyword +10 | case [x] | {1: x} | Class(x=1, x=2): ... # MatchOr + | + + + | + 7 | case {1: x, **x}: ... # MatchMapping duplicate in **rest + 8 | case Class(x, x): ... # MatchClass positional + 9 | case Class(x=1, x=2): ... # MatchClass keyword + | ^ Syntax Error: multiple assignments in pattern +10 | case [x] | {1: x} | Class(x=1, x=2): ... # MatchOr + | + + + | + 8 | case Class(x, x): ... # MatchClass positional + 9 | case Class(x=1, x=2): ... # MatchClass keyword +10 | case [x] | {1: x} | Class(x=1, x=2): ... # MatchOr + | ^ Syntax Error: multiple assignments in pattern + | diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@multiple_assignment_in_case_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@multiple_assignment_in_case_pattern.py.snap new file mode 100644 index 0000000000000..a12c28b913544 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@multiple_assignment_in_case_pattern.py.snap @@ -0,0 +1,115 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/multiple_assignment_in_case_pattern.py +snapshot_kind: text +--- +## AST + +``` +Module( + ModModule { + range: 0..42, + body: [ + Match( + StmtMatch { + range: 0..41, + subject: NumberLiteral( + ExprNumberLiteral { + range: 6..7, + value: Int( + 2, + ), + }, + ), + cases: [ + MatchCase { + range: 13..41, + pattern: MatchOr( + PatternMatchOr { + range: 18..36, + patterns: [ + MatchClass( + PatternMatchClass { + range: 18..26, + cls: Name( + ExprName { + range: 18..23, + id: Name("Class"), + ctx: Load, + }, + ), + arguments: PatternArguments { + range: 23..26, + patterns: [ + MatchAs( + PatternMatchAs { + range: 24..25, + pattern: None, + name: Some( + Identifier { + id: Name("x"), + range: 24..25, + }, + ), + }, + ), + ], + keywords: [], + }, + }, + ), + MatchSequence( + PatternMatchSequence { + range: 29..32, + patterns: [ + MatchAs( + PatternMatchAs { + range: 30..31, + pattern: None, + name: Some( + Identifier { + id: Name("x"), + range: 30..31, + }, + ), + }, + ), + ], + }, + ), + MatchAs( + PatternMatchAs { + range: 35..36, + pattern: None, + name: Some( + Identifier { + id: Name("x"), + range: 35..36, + }, + ), + }, + ), + ], + }, + ), + guard: None, + body: [ + Expr( + StmtExpr { + range: 38..41, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 38..41, + }, + ), + }, + ), + ], + }, + ], + }, + ), + ], + }, +) +``` From 230a0e59ffd6e149f16c2e06c253deacea6f5da4 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 24 Mar 2025 13:56:10 -0400 Subject: [PATCH 2/7] visit MatchAs in source order and avoid range check --- crates/ruff_python_parser/src/semantic_errors.rs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index 451bb9994e0f6..0edd5065c3360 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -331,15 +331,10 @@ impl<'a, Ctx: SemanticSyntaxContext> MultipleCaseAssignmentVisitor<'a, Ctx> { fn push(&mut self, other: &'a ast::Identifier) { for ident in &self.names { if other.id == ident.id { - let range = if ident.range.start() > other.range.start() { - ident.range - } else { - other.range - }; SemanticSyntaxChecker::add_error( self.ctx, SemanticSyntaxErrorKind::MultipleCaseAssignment, - range, + other.range, ); return; } @@ -393,12 +388,12 @@ impl<'a, Ctx: SemanticSyntaxContext> MultipleCaseAssignmentVisitor<'a, Ctx> { } } Pattern::MatchAs(ast::PatternMatchAs { pattern, name, .. }) => { - if let Some(name) = name { - self.push(name); - } if let Some(pattern) = pattern { self.visit_pattern(pattern); } + if let Some(name) = name { + self.push(name); + } } Pattern::MatchOr(ast::PatternMatchOr { patterns, .. }) => { // each of these patterns should be visited separately From f072b19dfa41badc9cd667ef84c58fcfaafade5b Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 24 Mar 2025 14:21:15 -0400 Subject: [PATCH 3/7] gather all names, sort and emit errors at the end --- .../ruff_python_parser/src/semantic_errors.rs | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index 0edd5065c3360..f7f1ba580e7b3 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -121,6 +121,7 @@ impl SemanticSyntaxChecker { ctx, }; visitor.visit_pattern(&case.pattern); + visitor.emit_errors(); } } @@ -327,19 +328,18 @@ struct MultipleCaseAssignmentVisitor<'a, Ctx> { } impl<'a, Ctx: SemanticSyntaxContext> MultipleCaseAssignmentVisitor<'a, Ctx> { - /// Check if `other` is present in `self.names` and emit an error if so. - fn push(&mut self, other: &'a ast::Identifier) { - for ident in &self.names { - if other.id == ident.id { + /// Emit [`SemanticSyntaxError`]s for every duplicate variable assignment. + fn emit_errors(mut self) { + self.names.sort_by_key(|name| &name.id); + for (n1, n2) in self.names.iter().zip(self.names.iter().skip(1)) { + if n1.id == n2.id { SemanticSyntaxChecker::add_error( self.ctx, SemanticSyntaxErrorKind::MultipleCaseAssignment, - other.range, + n2.range, ); - return; } } - self.names.push(other); } fn visit_pattern(&mut self, pattern: &'a Pattern) { @@ -362,7 +362,7 @@ impl<'a, Ctx: SemanticSyntaxContext> MultipleCaseAssignmentVisitor<'a, Ctx> { Pattern::MatchValue(_) | Pattern::MatchSingleton(_) => {} Pattern::MatchStar(ast::PatternMatchStar { name, .. }) => { if let Some(name) = name { - self.push(name); + self.names.push(name); } } Pattern::MatchSequence(ast::PatternMatchSequence { patterns, .. }) => { @@ -375,7 +375,7 @@ impl<'a, Ctx: SemanticSyntaxContext> MultipleCaseAssignmentVisitor<'a, Ctx> { self.visit_pattern(pattern); } if let Some(rest) = rest { - self.push(rest); + self.names.push(rest); } } Pattern::MatchClass(ast::PatternMatchClass { arguments, .. }) => { @@ -383,7 +383,7 @@ impl<'a, Ctx: SemanticSyntaxContext> MultipleCaseAssignmentVisitor<'a, Ctx> { self.visit_pattern(pattern); } for keyword in &arguments.keywords { - self.push(&keyword.attr); + self.names.push(&keyword.attr); self.visit_pattern(&keyword.pattern); } } @@ -392,7 +392,7 @@ impl<'a, Ctx: SemanticSyntaxContext> MultipleCaseAssignmentVisitor<'a, Ctx> { self.visit_pattern(pattern); } if let Some(name) = name { - self.push(name); + self.names.push(name); } } Pattern::MatchOr(ast::PatternMatchOr { patterns, .. }) => { @@ -403,6 +403,7 @@ impl<'a, Ctx: SemanticSyntaxContext> MultipleCaseAssignmentVisitor<'a, Ctx> { ctx: self.ctx, }; visitor.visit_pattern(pattern); + visitor.emit_errors(); } } } From d6f22d3ad2d39673e8574179e56565d97d435207 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 24 Mar 2025 14:32:33 -0400 Subject: [PATCH 4/7] add Name to error message --- crates/ruff_linter/src/checkers/ast/mod.rs | 4 ++-- .../ruff_python_parser/src/semantic_errors.rs | 12 +++++------ ...ultiple_assignment_in_case_pattern.py.snap | 20 +++++++++---------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 858ea7c74e965..bbcdf55d21983 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -549,14 +549,14 @@ impl SemanticSyntaxContext for Checker<'_> { } SemanticSyntaxErrorKind::ReboundComprehensionVariable | SemanticSyntaxErrorKind::DuplicateTypeParameter - | SemanticSyntaxErrorKind::MultipleCaseAssignment + | SemanticSyntaxErrorKind::MultipleCaseAssignment(_) if self.settings.preview.is_enabled() => { self.semantic_errors.borrow_mut().push(error); } SemanticSyntaxErrorKind::ReboundComprehensionVariable | SemanticSyntaxErrorKind::DuplicateTypeParameter - | SemanticSyntaxErrorKind::MultipleCaseAssignment => {} + | SemanticSyntaxErrorKind::MultipleCaseAssignment(_) => {} } } } diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index f7f1ba580e7b3..8bdb8aedb4018 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -225,7 +225,7 @@ pub struct SemanticSyntaxError { impl Display for SemanticSyntaxError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self.kind { + match &self.kind { SemanticSyntaxErrorKind::LateFutureImport => { f.write_str("__future__ imports must be at the top of the file") } @@ -235,14 +235,14 @@ impl Display for SemanticSyntaxError { SemanticSyntaxErrorKind::DuplicateTypeParameter => { f.write_str("duplicate type parameter") } - SemanticSyntaxErrorKind::MultipleCaseAssignment => { - f.write_str("multiple assignments in pattern") + SemanticSyntaxErrorKind::MultipleCaseAssignment(name) => { + write!(f, "multiple assignments to name `{name}` in pattern") } } } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum SemanticSyntaxErrorKind { /// Represents the use of a `__future__` import after the beginning of a file. /// @@ -295,7 +295,7 @@ pub enum SemanticSyntaxErrorKind { /// case x as x: ... /// case Class(x=1, x=2): ... /// ``` - MultipleCaseAssignment, + MultipleCaseAssignment(ast::name::Name), } /// Searches for the first named expression (`x := y`) rebinding one of the `iteration_variables` in @@ -335,7 +335,7 @@ impl<'a, Ctx: SemanticSyntaxContext> MultipleCaseAssignmentVisitor<'a, Ctx> { if n1.id == n2.id { SemanticSyntaxChecker::add_error( self.ctx, - SemanticSyntaxErrorKind::MultipleCaseAssignment, + SemanticSyntaxErrorKind::MultipleCaseAssignment(n2.id.clone()), n2.range, ); } diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_case_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_case_pattern.py.snap index 75694ff9344d1..3c91726f57706 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_case_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_case_pattern.py.snap @@ -630,7 +630,7 @@ Module( | 1 | match 2: 2 | case x as x: ... # MatchAs - | ^ Syntax Error: multiple assignments in pattern + | ^ Syntax Error: multiple assignments to name `x` in pattern 3 | case [y, z, y]: ... # MatchSequence 4 | case [y, z, *y]: ... # MatchSequence | @@ -640,7 +640,7 @@ Module( 1 | match 2: 2 | case x as x: ... # MatchAs 3 | case [y, z, y]: ... # MatchSequence - | ^ Syntax Error: multiple assignments in pattern + | ^ Syntax Error: multiple assignments to name `y` in pattern 4 | case [y, z, *y]: ... # MatchSequence 5 | case [y, y, y]: ... # MatchSequence multiple | @@ -650,7 +650,7 @@ Module( 2 | case x as x: ... # MatchAs 3 | case [y, z, y]: ... # MatchSequence 4 | case [y, z, *y]: ... # MatchSequence - | ^ Syntax Error: multiple assignments in pattern + | ^ Syntax Error: multiple assignments to name `y` in pattern 5 | case [y, y, y]: ... # MatchSequence multiple 6 | case {1: x, 2: x}: ... # MatchMapping duplicate pattern | @@ -660,7 +660,7 @@ Module( 3 | case [y, z, y]: ... # MatchSequence 4 | case [y, z, *y]: ... # MatchSequence 5 | case [y, y, y]: ... # MatchSequence multiple - | ^ Syntax Error: multiple assignments in pattern + | ^ Syntax Error: multiple assignments to name `y` in pattern 6 | case {1: x, 2: x}: ... # MatchMapping duplicate pattern 7 | case {1: x, **x}: ... # MatchMapping duplicate in **rest | @@ -670,7 +670,7 @@ Module( 3 | case [y, z, y]: ... # MatchSequence 4 | case [y, z, *y]: ... # MatchSequence 5 | case [y, y, y]: ... # MatchSequence multiple - | ^ Syntax Error: multiple assignments in pattern + | ^ Syntax Error: multiple assignments to name `y` in pattern 6 | case {1: x, 2: x}: ... # MatchMapping duplicate pattern 7 | case {1: x, **x}: ... # MatchMapping duplicate in **rest | @@ -680,7 +680,7 @@ Module( 4 | case [y, z, *y]: ... # MatchSequence 5 | case [y, y, y]: ... # MatchSequence multiple 6 | case {1: x, 2: x}: ... # MatchMapping duplicate pattern - | ^ Syntax Error: multiple assignments in pattern + | ^ Syntax Error: multiple assignments to name `x` in pattern 7 | case {1: x, **x}: ... # MatchMapping duplicate in **rest 8 | case Class(x, x): ... # MatchClass positional | @@ -690,7 +690,7 @@ Module( 5 | case [y, y, y]: ... # MatchSequence multiple 6 | case {1: x, 2: x}: ... # MatchMapping duplicate pattern 7 | case {1: x, **x}: ... # MatchMapping duplicate in **rest - | ^ Syntax Error: multiple assignments in pattern + | ^ Syntax Error: multiple assignments to name `x` in pattern 8 | case Class(x, x): ... # MatchClass positional 9 | case Class(x=1, x=2): ... # MatchClass keyword | @@ -700,7 +700,7 @@ Module( 6 | case {1: x, 2: x}: ... # MatchMapping duplicate pattern 7 | case {1: x, **x}: ... # MatchMapping duplicate in **rest 8 | case Class(x, x): ... # MatchClass positional - | ^ Syntax Error: multiple assignments in pattern + | ^ Syntax Error: multiple assignments to name `x` in pattern 9 | case Class(x=1, x=2): ... # MatchClass keyword 10 | case [x] | {1: x} | Class(x=1, x=2): ... # MatchOr | @@ -710,7 +710,7 @@ Module( 7 | case {1: x, **x}: ... # MatchMapping duplicate in **rest 8 | case Class(x, x): ... # MatchClass positional 9 | case Class(x=1, x=2): ... # MatchClass keyword - | ^ Syntax Error: multiple assignments in pattern + | ^ Syntax Error: multiple assignments to name `x` in pattern 10 | case [x] | {1: x} | Class(x=1, x=2): ... # MatchOr | @@ -719,5 +719,5 @@ Module( 8 | case Class(x, x): ... # MatchClass positional 9 | case Class(x=1, x=2): ... # MatchClass keyword 10 | case [x] | {1: x} | Class(x=1, x=2): ... # MatchOr - | ^ Syntax Error: multiple assignments in pattern + | ^ Syntax Error: multiple assignments to name `x` in pattern | From 343dc05408a75b8e381617dcb1af6db1838ba514 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 25 Mar 2025 10:41:26 -0400 Subject: [PATCH 5/7] expand comment on MatchOr and move test_ok case, use set --- .../ruff_python_parser/src/semantic_errors.rs | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index 8bdb8aedb4018..edd76653c4b50 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -12,6 +12,7 @@ use ruff_python_ast::{ Expr, Pattern, PythonVersion, Stmt, StmtExpr, StmtImportFrom, }; use ruff_text_size::{Ranged, TextRange}; +use rustc_hash::FxHashSet; #[derive(Debug)] pub struct SemanticSyntaxChecker { @@ -117,11 +118,10 @@ impl SemanticSyntaxChecker { for case in cases { let mut visitor = MultipleCaseAssignmentVisitor { - names: Vec::new(), + names: FxHashSet::default(), ctx, }; visitor.visit_pattern(&case.pattern); - visitor.emit_errors(); } } @@ -323,22 +323,19 @@ impl Visitor<'_> for ReboundComprehensionVisitor<'_> { } struct MultipleCaseAssignmentVisitor<'a, Ctx> { - names: Vec<&'a ast::Identifier>, + names: FxHashSet<&'a ast::name::Name>, ctx: &'a Ctx, } impl<'a, Ctx: SemanticSyntaxContext> MultipleCaseAssignmentVisitor<'a, Ctx> { /// Emit [`SemanticSyntaxError`]s for every duplicate variable assignment. - fn emit_errors(mut self) { - self.names.sort_by_key(|name| &name.id); - for (n1, n2) in self.names.iter().zip(self.names.iter().skip(1)) { - if n1.id == n2.id { - SemanticSyntaxChecker::add_error( - self.ctx, - SemanticSyntaxErrorKind::MultipleCaseAssignment(n2.id.clone()), - n2.range, - ); - } + fn push(&mut self, ident: &'a ast::Identifier) { + if !self.names.insert(&ident.id) { + SemanticSyntaxChecker::add_error( + self.ctx, + SemanticSyntaxErrorKind::MultipleCaseAssignment(ident.id.clone()), + ident.range(), + ); } } @@ -354,15 +351,11 @@ impl<'a, Ctx: SemanticSyntaxContext> MultipleCaseAssignmentVisitor<'a, Ctx> { // case Class(x, x): ... # MatchClass positional // case Class(x=1, x=2): ... # MatchClass keyword // case [x] | {1: x} | Class(x=1, x=2): ... # MatchOr - - // test_ok multiple_assignment_in_case_pattern - // match 2: - // case Class(x) | [x] | x: ... match pattern { Pattern::MatchValue(_) | Pattern::MatchSingleton(_) => {} Pattern::MatchStar(ast::PatternMatchStar { name, .. }) => { if let Some(name) = name { - self.names.push(name); + self.push(name); } } Pattern::MatchSequence(ast::PatternMatchSequence { patterns, .. }) => { @@ -375,7 +368,7 @@ impl<'a, Ctx: SemanticSyntaxContext> MultipleCaseAssignmentVisitor<'a, Ctx> { self.visit_pattern(pattern); } if let Some(rest) = rest { - self.names.push(rest); + self.push(rest); } } Pattern::MatchClass(ast::PatternMatchClass { arguments, .. }) => { @@ -383,7 +376,7 @@ impl<'a, Ctx: SemanticSyntaxContext> MultipleCaseAssignmentVisitor<'a, Ctx> { self.visit_pattern(pattern); } for keyword in &arguments.keywords { - self.names.push(&keyword.attr); + self.push(&keyword.attr); self.visit_pattern(&keyword.pattern); } } @@ -392,18 +385,23 @@ impl<'a, Ctx: SemanticSyntaxContext> MultipleCaseAssignmentVisitor<'a, Ctx> { self.visit_pattern(pattern); } if let Some(name) = name { - self.names.push(name); + self.push(name); } } Pattern::MatchOr(ast::PatternMatchOr { patterns, .. }) => { - // each of these patterns should be visited separately + // each of these patterns should be visited separately because patterns can only be + // duplicated within a single arm of the or pattern. For example, the case below is + // a valid pattern. + + // test_ok multiple_assignment_in_case_pattern + // match 2: + // case Class(x) | [x] | x: ... for pattern in patterns { let mut visitor = Self { - names: Vec::new(), + names: FxHashSet::default(), ctx: self.ctx, }; visitor.visit_pattern(pattern); - visitor.emit_errors(); } } } From aae81251c550fb8b7d74ad639440f7bf07e7d478 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 25 Mar 2025 13:27:37 -0400 Subject: [PATCH 6/7] push -> insert and move after visit_pattern --- .../ruff_python_parser/src/semantic_errors.rs | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index edd76653c4b50..42f1a2ad9dbdc 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -328,17 +328,6 @@ struct MultipleCaseAssignmentVisitor<'a, Ctx> { } impl<'a, Ctx: SemanticSyntaxContext> MultipleCaseAssignmentVisitor<'a, Ctx> { - /// Emit [`SemanticSyntaxError`]s for every duplicate variable assignment. - fn push(&mut self, ident: &'a ast::Identifier) { - if !self.names.insert(&ident.id) { - SemanticSyntaxChecker::add_error( - self.ctx, - SemanticSyntaxErrorKind::MultipleCaseAssignment(ident.id.clone()), - ident.range(), - ); - } - } - fn visit_pattern(&mut self, pattern: &'a Pattern) { // test_err multiple_assignment_in_case_pattern // match 2: @@ -355,7 +344,7 @@ impl<'a, Ctx: SemanticSyntaxContext> MultipleCaseAssignmentVisitor<'a, Ctx> { Pattern::MatchValue(_) | Pattern::MatchSingleton(_) => {} Pattern::MatchStar(ast::PatternMatchStar { name, .. }) => { if let Some(name) = name { - self.push(name); + self.insert(name); } } Pattern::MatchSequence(ast::PatternMatchSequence { patterns, .. }) => { @@ -368,7 +357,7 @@ impl<'a, Ctx: SemanticSyntaxContext> MultipleCaseAssignmentVisitor<'a, Ctx> { self.visit_pattern(pattern); } if let Some(rest) = rest { - self.push(rest); + self.insert(rest); } } Pattern::MatchClass(ast::PatternMatchClass { arguments, .. }) => { @@ -376,7 +365,7 @@ impl<'a, Ctx: SemanticSyntaxContext> MultipleCaseAssignmentVisitor<'a, Ctx> { self.visit_pattern(pattern); } for keyword in &arguments.keywords { - self.push(&keyword.attr); + self.insert(&keyword.attr); self.visit_pattern(&keyword.pattern); } } @@ -385,7 +374,7 @@ impl<'a, Ctx: SemanticSyntaxContext> MultipleCaseAssignmentVisitor<'a, Ctx> { self.visit_pattern(pattern); } if let Some(name) = name { - self.push(name); + self.insert(name); } } Pattern::MatchOr(ast::PatternMatchOr { patterns, .. }) => { @@ -406,6 +395,18 @@ impl<'a, Ctx: SemanticSyntaxContext> MultipleCaseAssignmentVisitor<'a, Ctx> { } } } + + /// Add an identifier to the set of visited names in `self` and emit a [`SemanticSyntaxError`] + /// if `ident` has already been seen. + fn insert(&mut self, ident: &'a ast::Identifier) { + if !self.names.insert(&ident.id) { + SemanticSyntaxChecker::add_error( + self.ctx, + SemanticSyntaxErrorKind::MultipleCaseAssignment(ident.id.clone()), + ident.range(), + ); + } + } } pub trait SemanticSyntaxContext { From 2e414cec073bc8294ad3853e39f3745ac8801abc Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 26 Mar 2025 12:48:50 -0400 Subject: [PATCH 7/7] pass StmtMatch to multiple_case_assignment too --- crates/ruff_python_parser/src/semantic_errors.rs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index 6591c11656b2a..4e2351a98351e 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -9,7 +9,7 @@ use std::fmt::Display; use ruff_python_ast::{ self as ast, visitor::{walk_expr, Visitor}, - Expr, Pattern,IrrefutablePatternKind, PythonVersion, Stmt, StmtExpr, StmtImportFrom, + Expr, IrrefutablePatternKind, Pattern, PythonVersion, Stmt, StmtExpr, StmtImportFrom, }; use ruff_text_size::{Ranged, TextRange}; use rustc_hash::FxHashSet; @@ -63,6 +63,7 @@ impl SemanticSyntaxChecker { } Stmt::Match(match_stmt) => { Self::irrefutable_match_case(match_stmt, ctx); + Self::multiple_case_assignment(match_stmt, ctx); } Stmt::FunctionDef(ast::StmtFunctionDef { type_params, .. }) | Stmt::ClassDef(ast::StmtClassDef { type_params, .. }) @@ -73,8 +74,6 @@ impl SemanticSyntaxChecker { } _ => {} } - - Self::multiple_case_assignment(stmt, ctx); } fn duplicate_type_parameter_name( @@ -115,12 +114,8 @@ impl SemanticSyntaxChecker { } } - fn multiple_case_assignment(stmt: &Stmt, ctx: &Ctx) { - let Stmt::Match(ast::StmtMatch { cases, .. }) = stmt else { - return; - }; - - for case in cases { + fn multiple_case_assignment(stmt: &ast::StmtMatch, ctx: &Ctx) { + for case in &stmt.cases { let mut visitor = MultipleCaseAssignmentVisitor { names: FxHashSet::default(), ctx,