[syntax-errors] Detect duplicate keys in match mapping patterns#17129
[syntax-errors] Detect duplicate keys in match mapping patterns#17129
match mapping patterns#17129Conversation
Summary -- Detects duplicate literals in `match` mapping keys. This PR also adds a `source` method to `SemanticSyntaxContext` to display the duplicated key in the error message by slicing out its range. Test Plan -- New inline tests.
| // 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()) |
There was a problem hiding this comment.
We could be a bit stricter here if we want (checking that the BinOp is an Add and the arguments are numbers), but we already report syntax errors for non-complex literals like 1 + 2, so I thought this might be sufficient.
There was a problem hiding this comment.
Yeah, I think that's fine, the parser should catch invalid complex literals.
There was a problem hiding this comment.
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.
I think f-strings are not allowed here by CPython, even without placeholders:
>>> match x:
... case {f"x": 1}: ...
...
File "<python-input-2>", line 2
case {f"x": 1}: ...
^^^^^^^^^
SyntaxError: mapping pattern keys may only match literals and attribute lookupsIn that case, I think our is_literal_expr is doing the right thing.
There was a problem hiding this comment.
Yeah, f-strings are not allowed as literal patterns.
|
dhruvmanila
left a comment
There was a problem hiding this comment.
Looks good, I'd suggest that we improve the diagnostic range and expand the check for all keys unless it creates an issue.
| Self::add_error( | ||
| ctx, | ||
| SemanticSyntaxErrorKind::DuplicateMatchKey(duplicate_key), | ||
| mapping.range, | ||
| ); | ||
| break; |
There was a problem hiding this comment.
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.
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.
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 :)
| /// ```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}: ... | ||
| /// ... | ||
| /// >>> | ||
| /// ``` |
There was a problem hiding this comment.
Interesting!
Also, I think you meant "python" or "py" and not "pycon" xD
There was a problem hiding this comment.
Oh I thought pycon was for the Python console [1]. That's what I use here on GitHub, although it doesn't usually do much syntax highlighting anyway.
There was a problem hiding this comment.
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.
| 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}: ... |
There was a problem hiding this comment.
What happens when there are multiple different duplicate keys? Like case {0: 1, "x": 1, 0: 2, "x": 2}: ....
I think we should highlight all the subsequent duplicate keys i.e., in the above case we should highlight the second 0 and the second "x" instance.
There was a problem hiding this comment.
Yeah CPython just notes the first one (also inline with the break above), but I think this is a good idea.
>>> match x:
... case {"x": 1, "x": 2, 0: 3, 0: 4}: ...
...
File "<python-input-224>", line 2
case {"x": 1, "x": 2, 0: 3, 0: 4}: ...
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: mapping pattern checks duplicate key ('x')| // 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()) |
There was a problem hiding this comment.
Yeah, I think that's fine, the parser should catch invalid complex literals.
| // 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()) |
There was a problem hiding this comment.
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).
| | | ||
| 1 | match x: | ||
| 2 | case {"x": 1, "x": 2}: ... | ||
| | ^^^ Syntax Error: mapping pattern checks duplicate key `"x"` |
There was a problem hiding this comment.
Another use case for multi-span diagostics :)
| | |_______^ Syntax Error: mapping pattern checks duplicate key `"""x | ||
| y | ||
| z | ||
| """` |
There was a problem hiding this comment.
This will break our concise rendering where each message should only be a single line long. I don't have a good recommendation but it's a general concern with including source text as is in diagnostic messages. You can either replace new lines, truncate before the new line (and replace with ...), or not include the name if it is multiline.
There was a problem hiding this comment.
I went with escaping the newlines to match CPython (as we discussed on Discord):
>>> match x:
... case {
... """x
... y
... z
... """: 1,
... """x
... y
... z
... """: 2}: ...
...
File "<python-input-0>", line 2
case {
^
SyntaxError: mapping pattern checks duplicate key ('x\n y\n z\n ')I added a modified version of std::str::EscapeDefault from the Rust standard library. It's a little more heavily modified than I wanted because many of the functions in the real implementation are private, but it gets the job done and without too much code.
* origin/main: [red-knot] Add `Type::TypeVar` variant (#17102) [red-knot] update to latest Salsa with fixpoint caching fix (#17179) Upgrade to Rust 1.86 and bump MSRV to 1.84 (#17171) [red-knot] Avoid unresolved-reference in unreachable code (#17169) Fix relative import resolution in `site-packages` packages when the `site-packages` search path is a subdirectory of the first-party search path (#17178) [DO NOT LAND] bump Salsa version (#17176) [syntax-errors] Detect duplicate keys in `match` mapping patterns (#17129)
…tral-sh#17129) Summary -- Detects duplicate literals in `match` mapping keys. This PR also adds a `source` method to `SemanticSyntaxContext` to display the duplicated key in the error message by slicing out its range. Test Plan -- New inline tests.
Summary
Detects duplicate literals in
matchmapping keys.This PR also adds a
sourcemethod toSemanticSyntaxContextto display the duplicated key in the error message by slicing out its range.Test Plan
New inline tests.