diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 4464adf9d2a6d3..fde85aa737db6d 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -573,6 +573,7 @@ impl SemanticSyntaxContext for Checker<'_> { | SemanticSyntaxErrorKind::IrrefutableCasePattern(_) | SemanticSyntaxErrorKind::SingleStarredAssignment | SemanticSyntaxErrorKind::WriteToDebug(_) + | SemanticSyntaxErrorKind::DuplicateMatchKey(_) | SemanticSyntaxErrorKind::InvalidStarExpression => { if self.settings.preview.is_enabled() { self.semantic_errors.borrow_mut().push(error); @@ -580,6 +581,10 @@ impl SemanticSyntaxContext for Checker<'_> { } } } + + fn source(&self) -> &str { + self.source() + } } impl<'a> Visitor<'a> for Checker<'a> { diff --git a/crates/ruff_python_parser/resources/inline/err/duplicate_match_key.py b/crates/ruff_python_parser/resources/inline/err/duplicate_match_key.py new file mode 100644 index 00000000000000..9f42626a115fa6 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/duplicate_match_key.py @@ -0,0 +1,19 @@ +match x: + case {"x": 1, "x": 2}: ... + case {b"x": 1, b"x": 2}: ... + case {0: 1, 0: 2}: ... + case {1.0: 1, 1.0: 2}: ... + case {1.0 + 2j: 1, 1.0 + 2j: 2}: ... + case {True: 1, True: 2}: ... + case {None: 1, None: 2}: ... + case { + """x + y + z + """: 1, + """x + y + z + """: 2}: ... + case {"x": 1, "x": 2, "x": 3}: ... + case {0: 1, "x": 1, 0: 2, "x": 2}: ... diff --git a/crates/ruff_python_parser/resources/inline/ok/duplicate_match_key_attr.py b/crates/ruff_python_parser/resources/inline/ok/duplicate_match_key_attr.py new file mode 100644 index 00000000000000..fab6ed6c068078 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/duplicate_match_key_attr.py @@ -0,0 +1,2 @@ +match x: + case {x.a: 1, x.a: 2}: ... diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index 0fac2fecf46063..4aa7fbeff36c64 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -8,6 +8,7 @@ use std::fmt::Display; use ruff_python_ast::{ self as ast, + comparable::ComparableExpr, visitor::{walk_expr, Visitor}, Expr, ExprContext, IrrefutablePatternKind, Pattern, PythonVersion, Stmt, StmtExpr, StmtImportFrom, @@ -65,6 +66,7 @@ impl SemanticSyntaxChecker { Stmt::Match(match_stmt) => { Self::irrefutable_match_case(match_stmt, ctx); Self::multiple_case_assignment(match_stmt, ctx); + Self::duplicate_match_mapping_keys(match_stmt, ctx); } Stmt::FunctionDef(ast::StmtFunctionDef { type_params, .. }) | Stmt::ClassDef(ast::StmtClassDef { type_params, .. }) @@ -270,6 +272,58 @@ impl SemanticSyntaxChecker { } } + fn duplicate_match_mapping_keys(stmt: &ast::StmtMatch, ctx: &Ctx) { + for mapping in stmt + .cases + .iter() + .filter_map(|case| case.pattern.as_match_mapping()) + { + let mut seen = FxHashSet::default(); + for key in mapping + .keys + .iter() + // complex numbers (`1 + 2j`) are allowed as keys but are not literals + // because they are represented as a `BinOp::Add` between a real number and + // an imaginary number + .filter(|key| key.is_literal_expr() || key.is_bin_op_expr()) + { + if !seen.insert(ComparableExpr::from(key)) { + let key_range = key.range(); + let duplicate_key = ctx.source()[key_range].to_string(); + // test_ok duplicate_match_key_attr + // match x: + // case {x.a: 1, x.a: 2}: ... + + // test_err duplicate_match_key + // match x: + // case {"x": 1, "x": 2}: ... + // case {b"x": 1, b"x": 2}: ... + // case {0: 1, 0: 2}: ... + // case {1.0: 1, 1.0: 2}: ... + // case {1.0 + 2j: 1, 1.0 + 2j: 2}: ... + // case {True: 1, True: 2}: ... + // case {None: 1, None: 2}: ... + // case { + // """x + // y + // z + // """: 1, + // """x + // y + // z + // """: 2}: ... + // case {"x": 1, "x": 2, "x": 3}: ... + // case {0: 1, "x": 1, 0: 2, "x": 2}: ... + Self::add_error( + ctx, + SemanticSyntaxErrorKind::DuplicateMatchKey(duplicate_key), + key_range, + ); + } + } + } + } + fn irrefutable_match_case(stmt: &ast::StmtMatch, ctx: &Ctx) { // test_ok irrefutable_case_pattern_at_end // match x: @@ -514,6 +568,13 @@ impl Display for SemanticSyntaxError { write!(f, "cannot delete `__debug__` on Python {python_version} (syntax was removed in 3.9)") } }, + SemanticSyntaxErrorKind::DuplicateMatchKey(key) => { + write!( + f, + "mapping pattern checks duplicate key `{}`", + EscapeDefault(key) + ) + } SemanticSyntaxErrorKind::LoadBeforeGlobalDeclaration { name, start: _ } => { write!(f, "name `{name}` is used prior to global declaration") } @@ -634,6 +695,41 @@ pub enum SemanticSyntaxErrorKind { /// [BPO 45000]: https://github.com/python/cpython/issues/89163 WriteToDebug(WriteToDebugKind), + /// Represents a duplicate key in a `match` mapping pattern. + /// + /// The [CPython grammar] allows keys in mapping patterns to be literals or attribute accesses: + /// + /// ```text + /// key_value_pattern: + /// | (literal_expr | attr) ':' pattern + /// ``` + /// + /// But only literals are checked for duplicates: + /// + /// ```pycon + /// >>> match x: + /// ... case {"x": 1, "x": 2}: ... + /// ... + /// File "", line 2 + /// case {"x": 1, "x": 2}: ... + /// ^^^^^^^^^^^^^^^^ + /// SyntaxError: mapping pattern checks duplicate key ('x') + /// >>> match x: + /// ... case {x.a: 1, x.a: 2}: ... + /// ... + /// >>> + /// ``` + /// + /// ## Examples + /// + /// ```python + /// match x: + /// case {"x": 1, "x": 2}: ... + /// ``` + /// + /// [CPython grammar]: https://docs.python.org/3/reference/grammar.html + DuplicateMatchKey(String), + /// Represents the use of a `global` variable before its `global` declaration. /// /// ## Examples @@ -789,6 +885,9 @@ pub trait SemanticSyntaxContext { /// The target Python version for detecting backwards-incompatible syntax changes. fn python_version(&self) -> PythonVersion; + /// Returns the source text under analysis. + fn source(&self) -> &str; + /// Return the [`TextRange`] at which a name is declared as `global` in the current scope. fn global(&self, name: &str) -> Option; @@ -828,3 +927,20 @@ where ruff_python_ast::visitor::walk_expr(self, expr); } } + +/// Modified version of [`std::str::EscapeDefault`] that does not escape single or double quotes. +struct EscapeDefault<'a>(&'a str); + +impl Display for EscapeDefault<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use std::fmt::Write; + + for c in self.0.chars() { + match c { + '\'' | '\"' => f.write_char(c)?, + _ => write!(f, "{}", c.escape_default())?, + } + } + Ok(()) + } +} diff --git a/crates/ruff_python_parser/tests/fixtures.rs b/crates/ruff_python_parser/tests/fixtures.rs index 47b8600470f300..f135709fdaefc7 100644 --- a/crates/ruff_python_parser/tests/fixtures.rs +++ b/crates/ruff_python_parser/tests/fixtures.rs @@ -89,7 +89,7 @@ fn test_valid_syntax(input_path: &Path) { let parsed = parsed.try_into_module().expect("Parsed with Mode::Module"); let mut visitor = SemanticSyntaxCheckerVisitor::new( - TestContext::default().with_python_version(options.target_version()), + TestContext::new(&source).with_python_version(options.target_version()), ); for stmt in parsed.suite() { @@ -185,7 +185,7 @@ fn test_invalid_syntax(input_path: &Path) { let parsed = parsed.try_into_module().expect("Parsed with Mode::Module"); let mut visitor = SemanticSyntaxCheckerVisitor::new( - TestContext::default().with_python_version(options.target_version()), + TestContext::new(&source).with_python_version(options.target_version()), ); for stmt in parsed.suite() { @@ -462,13 +462,22 @@ impl<'ast> SourceOrderVisitor<'ast> for ValidateAstVisitor<'ast> { } } -#[derive(Debug, Default)] -struct TestContext { +#[derive(Debug)] +struct TestContext<'a> { diagnostics: RefCell>, python_version: PythonVersion, + source: &'a str, } -impl TestContext { +impl<'a> TestContext<'a> { + fn new(source: &'a str) -> Self { + Self { + diagnostics: RefCell::default(), + python_version: PythonVersion::default(), + source, + } + } + #[must_use] fn with_python_version(mut self, python_version: PythonVersion) -> Self { self.python_version = python_version; @@ -476,7 +485,7 @@ impl TestContext { } } -impl SemanticSyntaxContext for TestContext { +impl SemanticSyntaxContext for TestContext<'_> { fn seen_docstring_boundary(&self) -> bool { false } @@ -489,6 +498,10 @@ impl SemanticSyntaxContext for TestContext { self.diagnostics.borrow_mut().push(error); } + fn source(&self) -> &str { + self.source + } + fn global(&self, _name: &str) -> Option { None } diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_match_key.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_match_key.py.snap new file mode 100644 index 00000000000000..9f41893b6028aa --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_match_key.py.snap @@ -0,0 +1,1022 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/duplicate_match_key.py +--- +## AST + +``` +Module( + ModModule { + range: 0..402, + body: [ + Match( + StmtMatch { + range: 0..401, + subject: Name( + ExprName { + range: 6..7, + id: Name("x"), + ctx: Load, + }, + ), + cases: [ + MatchCase { + range: 13..39, + pattern: MatchMapping( + PatternMatchMapping { + range: 18..34, + keys: [ + StringLiteral( + ExprStringLiteral { + range: 19..22, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 19..22, + value: "x", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + StringLiteral( + ExprStringLiteral { + range: 27..30, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 27..30, + value: "x", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + ], + patterns: [ + MatchValue( + PatternMatchValue { + range: 24..25, + value: NumberLiteral( + ExprNumberLiteral { + range: 24..25, + value: Int( + 1, + ), + }, + ), + }, + ), + MatchValue( + PatternMatchValue { + range: 32..33, + value: NumberLiteral( + ExprNumberLiteral { + range: 32..33, + value: Int( + 2, + ), + }, + ), + }, + ), + ], + rest: None, + }, + ), + guard: None, + body: [ + Expr( + StmtExpr { + range: 36..39, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 36..39, + }, + ), + }, + ), + ], + }, + MatchCase { + range: 44..72, + pattern: MatchMapping( + PatternMatchMapping { + range: 49..67, + keys: [ + BytesLiteral( + ExprBytesLiteral { + range: 50..54, + value: BytesLiteralValue { + inner: Single( + BytesLiteral { + range: 50..54, + value: [ + 120, + ], + flags: BytesLiteralFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + BytesLiteral( + ExprBytesLiteral { + range: 59..63, + value: BytesLiteralValue { + inner: Single( + BytesLiteral { + range: 59..63, + value: [ + 120, + ], + flags: BytesLiteralFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + ], + patterns: [ + MatchValue( + PatternMatchValue { + range: 56..57, + value: NumberLiteral( + ExprNumberLiteral { + range: 56..57, + value: Int( + 1, + ), + }, + ), + }, + ), + MatchValue( + PatternMatchValue { + range: 65..66, + value: NumberLiteral( + ExprNumberLiteral { + range: 65..66, + value: Int( + 2, + ), + }, + ), + }, + ), + ], + rest: None, + }, + ), + guard: None, + body: [ + Expr( + StmtExpr { + range: 69..72, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 69..72, + }, + ), + }, + ), + ], + }, + MatchCase { + range: 77..99, + pattern: MatchMapping( + PatternMatchMapping { + range: 82..94, + keys: [ + NumberLiteral( + ExprNumberLiteral { + range: 83..84, + value: Int( + 0, + ), + }, + ), + NumberLiteral( + ExprNumberLiteral { + range: 89..90, + value: Int( + 0, + ), + }, + ), + ], + patterns: [ + MatchValue( + PatternMatchValue { + range: 86..87, + value: NumberLiteral( + ExprNumberLiteral { + range: 86..87, + value: Int( + 1, + ), + }, + ), + }, + ), + MatchValue( + PatternMatchValue { + range: 92..93, + value: NumberLiteral( + ExprNumberLiteral { + range: 92..93, + value: Int( + 2, + ), + }, + ), + }, + ), + ], + rest: None, + }, + ), + guard: None, + body: [ + Expr( + StmtExpr { + range: 96..99, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 96..99, + }, + ), + }, + ), + ], + }, + MatchCase { + range: 104..130, + pattern: MatchMapping( + PatternMatchMapping { + range: 109..125, + keys: [ + NumberLiteral( + ExprNumberLiteral { + range: 110..113, + value: Float( + 1.0, + ), + }, + ), + NumberLiteral( + ExprNumberLiteral { + range: 118..121, + value: Float( + 1.0, + ), + }, + ), + ], + patterns: [ + MatchValue( + PatternMatchValue { + range: 115..116, + value: NumberLiteral( + ExprNumberLiteral { + range: 115..116, + value: Int( + 1, + ), + }, + ), + }, + ), + MatchValue( + PatternMatchValue { + range: 123..124, + value: NumberLiteral( + ExprNumberLiteral { + range: 123..124, + value: Int( + 2, + ), + }, + ), + }, + ), + ], + rest: None, + }, + ), + guard: None, + body: [ + Expr( + StmtExpr { + range: 127..130, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 127..130, + }, + ), + }, + ), + ], + }, + MatchCase { + range: 135..171, + pattern: MatchMapping( + PatternMatchMapping { + range: 140..166, + keys: [ + BinOp( + ExprBinOp { + range: 141..149, + left: NumberLiteral( + ExprNumberLiteral { + range: 141..144, + value: Float( + 1.0, + ), + }, + ), + op: Add, + right: NumberLiteral( + ExprNumberLiteral { + range: 147..149, + value: Complex { + real: 0.0, + imag: 2.0, + }, + }, + ), + }, + ), + BinOp( + ExprBinOp { + range: 154..162, + left: NumberLiteral( + ExprNumberLiteral { + range: 154..157, + value: Float( + 1.0, + ), + }, + ), + op: Add, + right: NumberLiteral( + ExprNumberLiteral { + range: 160..162, + value: Complex { + real: 0.0, + imag: 2.0, + }, + }, + ), + }, + ), + ], + patterns: [ + MatchValue( + PatternMatchValue { + range: 151..152, + value: NumberLiteral( + ExprNumberLiteral { + range: 151..152, + value: Int( + 1, + ), + }, + ), + }, + ), + MatchValue( + PatternMatchValue { + range: 164..165, + value: NumberLiteral( + ExprNumberLiteral { + range: 164..165, + value: Int( + 2, + ), + }, + ), + }, + ), + ], + rest: None, + }, + ), + guard: None, + body: [ + Expr( + StmtExpr { + range: 168..171, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 168..171, + }, + ), + }, + ), + ], + }, + MatchCase { + range: 176..204, + pattern: MatchMapping( + PatternMatchMapping { + range: 181..199, + keys: [ + BooleanLiteral( + ExprBooleanLiteral { + range: 182..186, + value: true, + }, + ), + BooleanLiteral( + ExprBooleanLiteral { + range: 191..195, + value: true, + }, + ), + ], + patterns: [ + MatchValue( + PatternMatchValue { + range: 188..189, + value: NumberLiteral( + ExprNumberLiteral { + range: 188..189, + value: Int( + 1, + ), + }, + ), + }, + ), + MatchValue( + PatternMatchValue { + range: 197..198, + value: NumberLiteral( + ExprNumberLiteral { + range: 197..198, + value: Int( + 2, + ), + }, + ), + }, + ), + ], + rest: None, + }, + ), + guard: None, + body: [ + Expr( + StmtExpr { + range: 201..204, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 201..204, + }, + ), + }, + ), + ], + }, + MatchCase { + range: 209..237, + pattern: MatchMapping( + PatternMatchMapping { + range: 214..232, + keys: [ + NoneLiteral( + ExprNoneLiteral { + range: 215..219, + }, + ), + NoneLiteral( + ExprNoneLiteral { + range: 224..228, + }, + ), + ], + patterns: [ + MatchValue( + PatternMatchValue { + range: 221..222, + value: NumberLiteral( + ExprNumberLiteral { + range: 221..222, + value: Int( + 1, + ), + }, + ), + }, + ), + MatchValue( + PatternMatchValue { + range: 230..231, + value: NumberLiteral( + ExprNumberLiteral { + range: 230..231, + value: Int( + 2, + ), + }, + ), + }, + ), + ], + rest: None, + }, + ), + guard: None, + body: [ + Expr( + StmtExpr { + range: 234..237, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 234..237, + }, + ), + }, + ), + ], + }, + MatchCase { + range: 242..319, + pattern: MatchMapping( + PatternMatchMapping { + range: 247..314, + keys: [ + StringLiteral( + ExprStringLiteral { + range: 253..277, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 253..277, + value: "x\n y\n z\n ", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: true, + }, + }, + ), + }, + }, + ), + StringLiteral( + ExprStringLiteral { + range: 286..310, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 286..310, + value: "x\n y\n z\n ", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: true, + }, + }, + ), + }, + }, + ), + ], + patterns: [ + MatchValue( + PatternMatchValue { + range: 279..280, + value: NumberLiteral( + ExprNumberLiteral { + range: 279..280, + value: Int( + 1, + ), + }, + ), + }, + ), + MatchValue( + PatternMatchValue { + range: 312..313, + value: NumberLiteral( + ExprNumberLiteral { + range: 312..313, + value: Int( + 2, + ), + }, + ), + }, + ), + ], + rest: None, + }, + ), + guard: None, + body: [ + Expr( + StmtExpr { + range: 316..319, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 316..319, + }, + ), + }, + ), + ], + }, + MatchCase { + range: 324..358, + pattern: MatchMapping( + PatternMatchMapping { + range: 329..353, + keys: [ + StringLiteral( + ExprStringLiteral { + range: 330..333, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 330..333, + value: "x", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + StringLiteral( + ExprStringLiteral { + range: 338..341, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 338..341, + value: "x", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + StringLiteral( + ExprStringLiteral { + range: 346..349, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 346..349, + value: "x", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + ], + patterns: [ + MatchValue( + PatternMatchValue { + range: 335..336, + value: NumberLiteral( + ExprNumberLiteral { + range: 335..336, + value: Int( + 1, + ), + }, + ), + }, + ), + MatchValue( + PatternMatchValue { + range: 343..344, + value: NumberLiteral( + ExprNumberLiteral { + range: 343..344, + value: Int( + 2, + ), + }, + ), + }, + ), + MatchValue( + PatternMatchValue { + range: 351..352, + value: NumberLiteral( + ExprNumberLiteral { + range: 351..352, + value: Int( + 3, + ), + }, + ), + }, + ), + ], + rest: None, + }, + ), + guard: None, + body: [ + Expr( + StmtExpr { + range: 355..358, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 355..358, + }, + ), + }, + ), + ], + }, + MatchCase { + range: 363..401, + pattern: MatchMapping( + PatternMatchMapping { + range: 368..396, + keys: [ + NumberLiteral( + ExprNumberLiteral { + range: 369..370, + value: Int( + 0, + ), + }, + ), + StringLiteral( + ExprStringLiteral { + range: 375..378, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 375..378, + value: "x", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + NumberLiteral( + ExprNumberLiteral { + range: 383..384, + value: Int( + 0, + ), + }, + ), + StringLiteral( + ExprStringLiteral { + range: 389..392, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 389..392, + value: "x", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + ], + patterns: [ + MatchValue( + PatternMatchValue { + range: 372..373, + value: NumberLiteral( + ExprNumberLiteral { + range: 372..373, + value: Int( + 1, + ), + }, + ), + }, + ), + MatchValue( + PatternMatchValue { + range: 380..381, + value: NumberLiteral( + ExprNumberLiteral { + range: 380..381, + value: Int( + 1, + ), + }, + ), + }, + ), + MatchValue( + PatternMatchValue { + range: 386..387, + value: NumberLiteral( + ExprNumberLiteral { + range: 386..387, + value: Int( + 2, + ), + }, + ), + }, + ), + MatchValue( + PatternMatchValue { + range: 394..395, + value: NumberLiteral( + ExprNumberLiteral { + range: 394..395, + value: Int( + 2, + ), + }, + ), + }, + ), + ], + rest: None, + }, + ), + guard: None, + body: [ + Expr( + StmtExpr { + range: 398..401, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 398..401, + }, + ), + }, + ), + ], + }, + ], + }, + ), + ], + }, +) +``` +## Semantic Syntax Errors + + | +1 | match x: +2 | case {"x": 1, "x": 2}: ... + | ^^^ Syntax Error: mapping pattern checks duplicate key `"x"` +3 | case {b"x": 1, b"x": 2}: ... +4 | case {0: 1, 0: 2}: ... + | + + + | +1 | match x: +2 | case {"x": 1, "x": 2}: ... +3 | case {b"x": 1, b"x": 2}: ... + | ^^^^ Syntax Error: mapping pattern checks duplicate key `b"x"` +4 | case {0: 1, 0: 2}: ... +5 | case {1.0: 1, 1.0: 2}: ... + | + + + | +2 | case {"x": 1, "x": 2}: ... +3 | case {b"x": 1, b"x": 2}: ... +4 | case {0: 1, 0: 2}: ... + | ^ Syntax Error: mapping pattern checks duplicate key `0` +5 | case {1.0: 1, 1.0: 2}: ... +6 | case {1.0 + 2j: 1, 1.0 + 2j: 2}: ... + | + + + | +3 | case {b"x": 1, b"x": 2}: ... +4 | case {0: 1, 0: 2}: ... +5 | case {1.0: 1, 1.0: 2}: ... + | ^^^ Syntax Error: mapping pattern checks duplicate key `1.0` +6 | case {1.0 + 2j: 1, 1.0 + 2j: 2}: ... +7 | case {True: 1, True: 2}: ... + | + + + | +4 | case {0: 1, 0: 2}: ... +5 | case {1.0: 1, 1.0: 2}: ... +6 | case {1.0 + 2j: 1, 1.0 + 2j: 2}: ... + | ^^^^^^^^ Syntax Error: mapping pattern checks duplicate key `1.0 + 2j` +7 | case {True: 1, True: 2}: ... +8 | case {None: 1, None: 2}: ... + | + + + | +5 | case {1.0: 1, 1.0: 2}: ... +6 | case {1.0 + 2j: 1, 1.0 + 2j: 2}: ... +7 | case {True: 1, True: 2}: ... + | ^^^^ Syntax Error: mapping pattern checks duplicate key `True` +8 | case {None: 1, None: 2}: ... +9 | case { + | + + + | + 6 | case {1.0 + 2j: 1, 1.0 + 2j: 2}: ... + 7 | case {True: 1, True: 2}: ... + 8 | case {None: 1, None: 2}: ... + | ^^^^ Syntax Error: mapping pattern checks duplicate key `None` + 9 | case { +10 | """x + | + + + | +12 | z +13 | """: 1, +14 | / """x +15 | | y +16 | | z +17 | | """: 2}: ... + | |_______^ Syntax Error: mapping pattern checks duplicate key `"""x\n y\n z\n """` +18 | case {"x": 1, "x": 2, "x": 3}: ... +19 | case {0: 1, "x": 1, 0: 2, "x": 2}: ... + | + + + | +16 | z +17 | """: 2}: ... +18 | case {"x": 1, "x": 2, "x": 3}: ... + | ^^^ Syntax Error: mapping pattern checks duplicate key `"x"` +19 | case {0: 1, "x": 1, 0: 2, "x": 2}: ... + | + + + | +16 | z +17 | """: 2}: ... +18 | case {"x": 1, "x": 2, "x": 3}: ... + | ^^^ Syntax Error: mapping pattern checks duplicate key `"x"` +19 | case {0: 1, "x": 1, 0: 2, "x": 2}: ... + | + + + | +17 | """: 2}: ... +18 | case {"x": 1, "x": 2, "x": 3}: ... +19 | case {0: 1, "x": 1, 0: 2, "x": 2}: ... + | ^ Syntax Error: mapping pattern checks duplicate key `0` + | + + + | +17 | """: 2}: ... +18 | case {"x": 1, "x": 2, "x": 3}: ... +19 | case {0: 1, "x": 1, 0: 2, "x": 2}: ... + | ^^^ Syntax Error: mapping pattern checks duplicate key `"x"` + | diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@duplicate_match_key_attr.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@duplicate_match_key_attr.py.snap new file mode 100644 index 00000000000000..56b342c8b5eb2e --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@duplicate_match_key_attr.py.snap @@ -0,0 +1,115 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/duplicate_match_key_attr.py +--- +## AST + +``` +Module( + ModModule { + range: 0..40, + body: [ + Match( + StmtMatch { + range: 0..39, + subject: Name( + ExprName { + range: 6..7, + id: Name("x"), + ctx: Load, + }, + ), + cases: [ + MatchCase { + range: 13..39, + pattern: MatchMapping( + PatternMatchMapping { + range: 18..34, + keys: [ + Attribute( + ExprAttribute { + range: 19..22, + value: Name( + ExprName { + range: 19..20, + id: Name("x"), + ctx: Load, + }, + ), + attr: Identifier { + id: Name("a"), + range: 21..22, + }, + ctx: Load, + }, + ), + Attribute( + ExprAttribute { + range: 27..30, + value: Name( + ExprName { + range: 27..28, + id: Name("x"), + ctx: Load, + }, + ), + attr: Identifier { + id: Name("a"), + range: 29..30, + }, + ctx: Load, + }, + ), + ], + patterns: [ + MatchValue( + PatternMatchValue { + range: 24..25, + value: NumberLiteral( + ExprNumberLiteral { + range: 24..25, + value: Int( + 1, + ), + }, + ), + }, + ), + MatchValue( + PatternMatchValue { + range: 32..33, + value: NumberLiteral( + ExprNumberLiteral { + range: 32..33, + value: Int( + 2, + ), + }, + ), + }, + ), + ], + rest: None, + }, + ), + guard: None, + body: [ + Expr( + StmtExpr { + range: 36..39, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 36..39, + }, + ), + }, + ), + ], + }, + ], + }, + ), + ], + }, +) +```