-
Notifications
You must be signed in to change notification settings - Fork 1.4k
[syntax-errors] Detect duplicate keys in match
mapping patterns
#17129
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
a0c7cce
3bab92e
47635c1
0ed466a
0abcc18
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
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}: ... |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
match x: | ||
case {x.a: 1, x.a: 2}: ... |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, .. }) | ||
|
@@ -240,6 +242,56 @@ impl SemanticSyntaxChecker { | |
} | ||
} | ||
|
||
fn duplicate_match_mapping_keys<Ctx: SemanticSyntaxContext>(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 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}: ... | ||
Self::add_error( | ||
ctx, | ||
SemanticSyntaxErrorKind::DuplicateMatchKey(duplicate_key), | ||
mapping.range, | ||
); | ||
break; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should avoid breaking here and report all duplicate keys within a single mapping pattern. Or, do you see any limitation or challenges in that approach? What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was just copying CPython, but that sounds reasonable too! >>> match x:
... case {"x": 1, "x": 2}: ...
...
File "<python-input-223>", line 2
case {"x": 1, "x": 2}: ...
^^^^^^^^^^^^^^^^
SyntaxError: mapping pattern checks duplicate key ('x') There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, in general, we should prefer to surface all errors whenever possible which is one of the main motivation to have an error resilient parser :) |
||
} | ||
} | ||
} | ||
} | ||
|
||
fn irrefutable_match_case<Ctx: SemanticSyntaxContext>(stmt: &ast::StmtMatch, ctx: &Ctx) { | ||
// test_ok irrefutable_case_pattern_at_end | ||
// match x: | ||
|
@@ -462,6 +514,9 @@ 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 `{key}`") | ||
} | ||
} | ||
} | ||
} | ||
|
@@ -575,6 +630,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 "<python-input-160>", line 2 | ||
/// case {"x": 1, "x": 2}: ... | ||
/// ^^^^^^^^^^^^^^^^ | ||
/// SyntaxError: mapping pattern checks duplicate key ('x') | ||
/// >>> match x: | ||
/// ... case {x.a: 1, x.a: 2}: ... | ||
/// ... | ||
/// >>> | ||
/// ``` | ||
Comment on lines
+709
to
+721
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting! Also, I think you meant "python" or "py" and not "pycon" xD There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh I thought There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh lol, I had no idea about that. I don't think that matters as those are going to be rendered in an editor or crates.io. |
||
/// | ||
/// ## Examples | ||
/// | ||
/// ```python | ||
/// match x: | ||
/// case {"x": 1, "x": 2}: ... | ||
/// ``` | ||
/// | ||
/// [CPython grammar]: https://docs.python.org/3/reference/grammar.html | ||
DuplicateMatchKey(String), | ||
} | ||
|
||
#[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
|
@@ -705,6 +795,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; | ||
|
||
fn report_semantic_error(&self, error: SemanticSyntaxError); | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could be a bit stricter here if we want (checking that the
BinOp
is anAdd
and the arguments are numbers), but we already report syntax errors for non-complex literals like1 + 2
, so I thought this might be sufficient.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think that's fine, the parser should catch invalid complex literals.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We also have to allow f-string expressions. They're allowed for as long as they contain no placeholders and they should compare equal to their string equivalent (I think this is already handled by
ComparableExpr
).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think f-strings are not allowed here by CPython, even without placeholders:
In that case, I think our
is_literal_expr
is doing the right thing.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, f-strings are not allowed as literal patterns.