[syntax-errors] Duplicate attributes in match class pattern (#17186)

Summary
--

Detects duplicate attributes in a `match` class pattern:

```python
match x:
    case Class(x=1, x=2): ...
```

which are more analogous to the similar check for mapping patterns than
to the
multiple assignments rule.

I also realized that both this and the mapping check would only work on
top-level patterns, despite the possibility that they can be nested
inside other
patterns:

```python
match x:
    case [{"x": 1, "x": 2}]: ...  # false negative in the old version
```

and moved these checks into the recursive pattern visitor instead.

I also tidied up some of the names like the `multiple_case_assignment`
function
and the `MultipleCaseAssignmentVisitor`, which are now doing more than
checking
for multiple assignments.

Test Plan
--

New inline tests for both classes and mappings.
This commit is contained in:
Brent Westbrook 2025-04-03 17:55:37 -04:00 committed by GitHub
parent 6a07dd227d
commit 24b1b1d52c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 1257 additions and 69 deletions

View file

@ -0,0 +1,715 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/duplicate_match_class_attr.py
---
## AST
```
Module(
ModModule {
range: 0..231,
body: [
Match(
StmtMatch {
range: 0..230,
subject: Name(
ExprName {
range: 6..7,
id: Name("x"),
ctx: Load,
},
),
cases: [
MatchCase {
range: 13..38,
pattern: MatchClass(
PatternMatchClass {
range: 18..33,
cls: Name(
ExprName {
range: 18..23,
id: Name("Class"),
ctx: Load,
},
),
arguments: PatternArguments {
range: 23..33,
patterns: [],
keywords: [
PatternKeyword {
range: 24..27,
attr: Identifier {
id: Name("x"),
range: 24..25,
},
pattern: MatchValue(
PatternMatchValue {
range: 26..27,
value: NumberLiteral(
ExprNumberLiteral {
range: 26..27,
value: Int(
1,
),
},
),
},
),
},
PatternKeyword {
range: 29..32,
attr: Identifier {
id: Name("x"),
range: 29..30,
},
pattern: MatchValue(
PatternMatchValue {
range: 31..32,
value: NumberLiteral(
ExprNumberLiteral {
range: 31..32,
value: Int(
2,
),
},
),
},
),
},
],
},
},
),
guard: None,
body: [
Expr(
StmtExpr {
range: 35..38,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 35..38,
},
),
},
),
],
},
MatchCase {
range: 43..70,
pattern: MatchSequence(
PatternMatchSequence {
range: 48..65,
patterns: [
MatchClass(
PatternMatchClass {
range: 49..64,
cls: Name(
ExprName {
range: 49..54,
id: Name("Class"),
ctx: Load,
},
),
arguments: PatternArguments {
range: 54..64,
patterns: [],
keywords: [
PatternKeyword {
range: 55..58,
attr: Identifier {
id: Name("x"),
range: 55..56,
},
pattern: MatchValue(
PatternMatchValue {
range: 57..58,
value: NumberLiteral(
ExprNumberLiteral {
range: 57..58,
value: Int(
1,
),
},
),
},
),
},
PatternKeyword {
range: 60..63,
attr: Identifier {
id: Name("x"),
range: 60..61,
},
pattern: MatchValue(
PatternMatchValue {
range: 62..63,
value: NumberLiteral(
ExprNumberLiteral {
range: 62..63,
value: Int(
2,
),
},
),
},
),
},
],
},
},
),
],
},
),
guard: None,
body: [
Expr(
StmtExpr {
range: 67..70,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 67..70,
},
),
},
),
],
},
MatchCase {
range: 75..113,
pattern: MatchMapping(
PatternMatchMapping {
range: 80..108,
keys: [
StringLiteral(
ExprStringLiteral {
range: 81..84,
value: StringLiteralValue {
inner: Single(
StringLiteral {
range: 81..84,
value: "x",
flags: StringLiteralFlags {
quote_style: Double,
prefix: Empty,
triple_quoted: false,
},
},
),
},
},
),
StringLiteral(
ExprStringLiteral {
range: 89..92,
value: StringLiteralValue {
inner: Single(
StringLiteral {
range: 89..92,
value: "y",
flags: StringLiteralFlags {
quote_style: Double,
prefix: Empty,
triple_quoted: false,
},
},
),
},
},
),
],
patterns: [
MatchAs(
PatternMatchAs {
range: 86..87,
pattern: None,
name: Some(
Identifier {
id: Name("x"),
range: 86..87,
},
),
},
),
MatchClass(
PatternMatchClass {
range: 94..107,
cls: Name(
ExprName {
range: 94..97,
id: Name("Foo"),
ctx: Load,
},
),
arguments: PatternArguments {
range: 97..107,
patterns: [],
keywords: [
PatternKeyword {
range: 98..101,
attr: Identifier {
id: Name("x"),
range: 98..99,
},
pattern: MatchValue(
PatternMatchValue {
range: 100..101,
value: NumberLiteral(
ExprNumberLiteral {
range: 100..101,
value: Int(
1,
),
},
),
},
),
},
PatternKeyword {
range: 103..106,
attr: Identifier {
id: Name("x"),
range: 103..104,
},
pattern: MatchValue(
PatternMatchValue {
range: 105..106,
value: NumberLiteral(
ExprNumberLiteral {
range: 105..106,
value: Int(
2,
),
},
),
},
),
},
],
},
},
),
],
rest: None,
},
),
guard: None,
body: [
Expr(
StmtExpr {
range: 110..113,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 110..113,
},
),
},
),
],
},
MatchCase {
range: 118..162,
pattern: MatchSequence(
PatternMatchSequence {
range: 123..157,
patterns: [
MatchMapping(
PatternMatchMapping {
range: 124..126,
keys: [],
patterns: [],
rest: None,
},
),
MatchMapping(
PatternMatchMapping {
range: 128..156,
keys: [
StringLiteral(
ExprStringLiteral {
range: 129..132,
value: StringLiteralValue {
inner: Single(
StringLiteral {
range: 129..132,
value: "x",
flags: StringLiteralFlags {
quote_style: Double,
prefix: Empty,
triple_quoted: false,
},
},
),
},
},
),
StringLiteral(
ExprStringLiteral {
range: 137..140,
value: StringLiteralValue {
inner: Single(
StringLiteral {
range: 137..140,
value: "y",
flags: StringLiteralFlags {
quote_style: Double,
prefix: Empty,
triple_quoted: false,
},
},
),
},
},
),
],
patterns: [
MatchAs(
PatternMatchAs {
range: 134..135,
pattern: None,
name: Some(
Identifier {
id: Name("x"),
range: 134..135,
},
),
},
),
MatchClass(
PatternMatchClass {
range: 142..155,
cls: Name(
ExprName {
range: 142..145,
id: Name("Foo"),
ctx: Load,
},
),
arguments: PatternArguments {
range: 145..155,
patterns: [],
keywords: [
PatternKeyword {
range: 146..149,
attr: Identifier {
id: Name("x"),
range: 146..147,
},
pattern: MatchValue(
PatternMatchValue {
range: 148..149,
value: NumberLiteral(
ExprNumberLiteral {
range: 148..149,
value: Int(
1,
),
},
),
},
),
},
PatternKeyword {
range: 151..154,
attr: Identifier {
id: Name("x"),
range: 151..152,
},
pattern: MatchValue(
PatternMatchValue {
range: 153..154,
value: NumberLiteral(
ExprNumberLiteral {
range: 153..154,
value: Int(
2,
),
},
),
},
),
},
],
},
},
),
],
rest: None,
},
),
],
},
),
guard: None,
body: [
Expr(
StmtExpr {
range: 159..162,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 159..162,
},
),
},
),
],
},
MatchCase {
range: 167..230,
pattern: MatchClass(
PatternMatchClass {
range: 172..225,
cls: Name(
ExprName {
range: 172..177,
id: Name("Class"),
ctx: Load,
},
),
arguments: PatternArguments {
range: 177..225,
patterns: [],
keywords: [
PatternKeyword {
range: 178..181,
attr: Identifier {
id: Name("x"),
range: 178..179,
},
pattern: MatchValue(
PatternMatchValue {
range: 180..181,
value: NumberLiteral(
ExprNumberLiteral {
range: 180..181,
value: Int(
1,
),
},
),
},
),
},
PatternKeyword {
range: 183..201,
attr: Identifier {
id: Name("d"),
range: 183..184,
},
pattern: MatchMapping(
PatternMatchMapping {
range: 185..201,
keys: [
StringLiteral(
ExprStringLiteral {
range: 186..189,
value: StringLiteralValue {
inner: Single(
StringLiteral {
range: 186..189,
value: "x",
flags: StringLiteralFlags {
quote_style: Double,
prefix: Empty,
triple_quoted: false,
},
},
),
},
},
),
StringLiteral(
ExprStringLiteral {
range: 194..197,
value: StringLiteralValue {
inner: Single(
StringLiteral {
range: 194..197,
value: "x",
flags: StringLiteralFlags {
quote_style: Double,
prefix: Empty,
triple_quoted: false,
},
},
),
},
},
),
],
patterns: [
MatchValue(
PatternMatchValue {
range: 191..192,
value: NumberLiteral(
ExprNumberLiteral {
range: 191..192,
value: Int(
1,
),
},
),
},
),
MatchValue(
PatternMatchValue {
range: 199..200,
value: NumberLiteral(
ExprNumberLiteral {
range: 199..200,
value: Int(
2,
),
},
),
},
),
],
rest: None,
},
),
},
PatternKeyword {
range: 203..224,
attr: Identifier {
id: Name("other"),
range: 203..208,
},
pattern: MatchClass(
PatternMatchClass {
range: 209..224,
cls: Name(
ExprName {
range: 209..214,
id: Name("Class"),
ctx: Load,
},
),
arguments: PatternArguments {
range: 214..224,
patterns: [],
keywords: [
PatternKeyword {
range: 215..218,
attr: Identifier {
id: Name("x"),
range: 215..216,
},
pattern: MatchValue(
PatternMatchValue {
range: 217..218,
value: NumberLiteral(
ExprNumberLiteral {
range: 217..218,
value: Int(
1,
),
},
),
},
),
},
PatternKeyword {
range: 220..223,
attr: Identifier {
id: Name("x"),
range: 220..221,
},
pattern: MatchValue(
PatternMatchValue {
range: 222..223,
value: NumberLiteral(
ExprNumberLiteral {
range: 222..223,
value: Int(
2,
),
},
),
},
),
},
],
},
},
),
},
],
},
},
),
guard: None,
body: [
Expr(
StmtExpr {
range: 227..230,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 227..230,
},
),
},
),
],
},
],
},
),
],
},
)
```
## Semantic Syntax Errors
|
1 | match x:
2 | case Class(x=1, x=2): ...
| ^ Syntax Error: attribute name `x` repeated in class pattern
3 | case [Class(x=1, x=2)]: ...
4 | case {"x": x, "y": Foo(x=1, x=2)}: ...
|
|
1 | match x:
2 | case Class(x=1, x=2): ...
3 | case [Class(x=1, x=2)]: ...
| ^ Syntax Error: attribute name `x` repeated in class pattern
4 | case {"x": x, "y": Foo(x=1, x=2)}: ...
5 | case [{}, {"x": x, "y": Foo(x=1, x=2)}]: ...
|
|
2 | case Class(x=1, x=2): ...
3 | case [Class(x=1, x=2)]: ...
4 | case {"x": x, "y": Foo(x=1, x=2)}: ...
| ^ Syntax Error: attribute name `x` repeated in class pattern
5 | case [{}, {"x": x, "y": Foo(x=1, x=2)}]: ...
6 | case Class(x=1, d={"x": 1, "x": 2}, other=Class(x=1, x=2)): ...
|
|
3 | case [Class(x=1, x=2)]: ...
4 | case {"x": x, "y": Foo(x=1, x=2)}: ...
5 | case [{}, {"x": x, "y": Foo(x=1, x=2)}]: ...
| ^ Syntax Error: attribute name `x` repeated in class pattern
6 | case Class(x=1, d={"x": 1, "x": 2}, other=Class(x=1, x=2)): ...
|
|
4 | case {"x": x, "y": Foo(x=1, x=2)}: ...
5 | case [{}, {"x": x, "y": Foo(x=1, x=2)}]: ...
6 | case Class(x=1, d={"x": 1, "x": 2}, other=Class(x=1, x=2)): ...
| ^^^ Syntax Error: mapping pattern checks duplicate key `"x"`
|
|
4 | case {"x": x, "y": Foo(x=1, x=2)}: ...
5 | case [{}, {"x": x, "y": Foo(x=1, x=2)}]: ...
6 | case Class(x=1, d={"x": 1, "x": 2}, other=Class(x=1, x=2)): ...
| ^ Syntax Error: attribute name `x` repeated in class pattern
|

View file

@ -7,11 +7,11 @@ input_file: crates/ruff_python_parser/resources/inline/err/duplicate_match_key.p
```
Module(
ModModule {
range: 0..402,
range: 0..533,
body: [
Match(
StmtMatch {
range: 0..401,
range: 0..532,
subject: Name(
ExprName {
range: 6..7,
@ -897,6 +897,412 @@ Module(
),
],
},
MatchCase {
range: 406..434,
pattern: MatchSequence(
PatternMatchSequence {
range: 411..429,
patterns: [
MatchMapping(
PatternMatchMapping {
range: 412..428,
keys: [
StringLiteral(
ExprStringLiteral {
range: 413..416,
value: StringLiteralValue {
inner: Single(
StringLiteral {
range: 413..416,
value: "x",
flags: StringLiteralFlags {
quote_style: Double,
prefix: Empty,
triple_quoted: false,
},
},
),
},
},
),
StringLiteral(
ExprStringLiteral {
range: 421..424,
value: StringLiteralValue {
inner: Single(
StringLiteral {
range: 421..424,
value: "x",
flags: StringLiteralFlags {
quote_style: Double,
prefix: Empty,
triple_quoted: false,
},
},
),
},
},
),
],
patterns: [
MatchValue(
PatternMatchValue {
range: 418..419,
value: NumberLiteral(
ExprNumberLiteral {
range: 418..419,
value: Int(
1,
),
},
),
},
),
MatchValue(
PatternMatchValue {
range: 426..427,
value: NumberLiteral(
ExprNumberLiteral {
range: 426..427,
value: Int(
2,
),
},
),
},
),
],
rest: None,
},
),
],
},
),
guard: None,
body: [
Expr(
StmtExpr {
range: 431..434,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 431..434,
},
),
},
),
],
},
MatchCase {
range: 439..477,
pattern: MatchClass(
PatternMatchClass {
range: 444..472,
cls: Name(
ExprName {
range: 444..447,
id: Name("Foo"),
ctx: Load,
},
),
arguments: PatternArguments {
range: 447..472,
patterns: [],
keywords: [
PatternKeyword {
range: 448..451,
attr: Identifier {
id: Name("x"),
range: 448..449,
},
pattern: MatchValue(
PatternMatchValue {
range: 450..451,
value: NumberLiteral(
ExprNumberLiteral {
range: 450..451,
value: Int(
1,
),
},
),
},
),
},
PatternKeyword {
range: 453..471,
attr: Identifier {
id: Name("y"),
range: 453..454,
},
pattern: MatchMapping(
PatternMatchMapping {
range: 455..471,
keys: [
StringLiteral(
ExprStringLiteral {
range: 456..459,
value: StringLiteralValue {
inner: Single(
StringLiteral {
range: 456..459,
value: "x",
flags: StringLiteralFlags {
quote_style: Double,
prefix: Empty,
triple_quoted: false,
},
},
),
},
},
),
StringLiteral(
ExprStringLiteral {
range: 464..467,
value: StringLiteralValue {
inner: Single(
StringLiteral {
range: 464..467,
value: "x",
flags: StringLiteralFlags {
quote_style: Double,
prefix: Empty,
triple_quoted: false,
},
},
),
},
},
),
],
patterns: [
MatchValue(
PatternMatchValue {
range: 461..462,
value: NumberLiteral(
ExprNumberLiteral {
range: 461..462,
value: Int(
1,
),
},
),
},
),
MatchValue(
PatternMatchValue {
range: 469..470,
value: NumberLiteral(
ExprNumberLiteral {
range: 469..470,
value: Int(
2,
),
},
),
},
),
],
rest: None,
},
),
},
],
},
},
),
guard: None,
body: [
Expr(
StmtExpr {
range: 474..477,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 474..477,
},
),
},
),
],
},
MatchCase {
range: 482..532,
pattern: MatchSequence(
PatternMatchSequence {
range: 487..527,
patterns: [
MatchClass(
PatternMatchClass {
range: 488..496,
cls: Name(
ExprName {
range: 488..491,
id: Name("Foo"),
ctx: Load,
},
),
arguments: PatternArguments {
range: 491..496,
patterns: [],
keywords: [
PatternKeyword {
range: 492..495,
attr: Identifier {
id: Name("x"),
range: 492..493,
},
pattern: MatchValue(
PatternMatchValue {
range: 494..495,
value: NumberLiteral(
ExprNumberLiteral {
range: 494..495,
value: Int(
1,
),
},
),
},
),
},
],
},
},
),
MatchClass(
PatternMatchClass {
range: 498..526,
cls: Name(
ExprName {
range: 498..501,
id: Name("Foo"),
ctx: Load,
},
),
arguments: PatternArguments {
range: 501..526,
patterns: [],
keywords: [
PatternKeyword {
range: 502..505,
attr: Identifier {
id: Name("x"),
range: 502..503,
},
pattern: MatchValue(
PatternMatchValue {
range: 504..505,
value: NumberLiteral(
ExprNumberLiteral {
range: 504..505,
value: Int(
1,
),
},
),
},
),
},
PatternKeyword {
range: 507..525,
attr: Identifier {
id: Name("y"),
range: 507..508,
},
pattern: MatchMapping(
PatternMatchMapping {
range: 509..525,
keys: [
StringLiteral(
ExprStringLiteral {
range: 510..513,
value: StringLiteralValue {
inner: Single(
StringLiteral {
range: 510..513,
value: "x",
flags: StringLiteralFlags {
quote_style: Double,
prefix: Empty,
triple_quoted: false,
},
},
),
},
},
),
StringLiteral(
ExprStringLiteral {
range: 518..521,
value: StringLiteralValue {
inner: Single(
StringLiteral {
range: 518..521,
value: "x",
flags: StringLiteralFlags {
quote_style: Double,
prefix: Empty,
triple_quoted: false,
},
},
),
},
},
),
],
patterns: [
MatchValue(
PatternMatchValue {
range: 515..516,
value: NumberLiteral(
ExprNumberLiteral {
range: 515..516,
value: Int(
1,
),
},
),
},
),
MatchValue(
PatternMatchValue {
range: 523..524,
value: NumberLiteral(
ExprNumberLiteral {
range: 523..524,
value: Int(
2,
),
},
),
},
),
],
rest: None,
},
),
},
],
},
},
),
],
},
),
guard: None,
body: [
Expr(
StmtExpr {
range: 529..532,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 529..532,
},
),
},
),
],
},
],
},
),
@ -994,6 +1400,7 @@ Module(
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}: ...
20 | case [{"x": 1, "x": 2}]: ...
|
@ -1003,6 +1410,7 @@ Module(
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}: ...
20 | case [{"x": 1, "x": 2}]: ...
|
@ -1011,6 +1419,8 @@ Module(
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`
20 | case [{"x": 1, "x": 2}]: ...
21 | case Foo(x=1, y={"x": 1, "x": 2}): ...
|
@ -1019,4 +1429,33 @@ Module(
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"`
20 | case [{"x": 1, "x": 2}]: ...
21 | case Foo(x=1, y={"x": 1, "x": 2}): ...
|
|
18 | case {"x": 1, "x": 2, "x": 3}: ...
19 | case {0: 1, "x": 1, 0: 2, "x": 2}: ...
20 | case [{"x": 1, "x": 2}]: ...
| ^^^ Syntax Error: mapping pattern checks duplicate key `"x"`
21 | case Foo(x=1, y={"x": 1, "x": 2}): ...
22 | case [Foo(x=1), Foo(x=1, y={"x": 1, "x": 2})]: ...
|
|
19 | case {0: 1, "x": 1, 0: 2, "x": 2}: ...
20 | case [{"x": 1, "x": 2}]: ...
21 | case Foo(x=1, y={"x": 1, "x": 2}): ...
| ^^^ Syntax Error: mapping pattern checks duplicate key `"x"`
22 | case [Foo(x=1), Foo(x=1, y={"x": 1, "x": 2})]: ...
|
|
20 | case [{"x": 1, "x": 2}]: ...
21 | case Foo(x=1, y={"x": 1, "x": 2}): ...
22 | case [Foo(x=1), Foo(x=1, y={"x": 1, "x": 2})]: ...
| ^^^ Syntax Error: mapping pattern checks duplicate key `"x"`
|