Skip to content

Conversation

@ntBre
Copy link
Contributor

@ntBre ntBre commented Mar 24, 2025

Summary

This PR detects multiple assignments to the same name in case patterns by recursively visiting each pattern.

Test Plan

New inline tests.

Summary
--

This PR detects multiple assignments to the same name in `case` patterns by
recursively visiting each pattern.

Test Plan
--

New inline tests.
@ntBre ntBre added the rule Implementing or modifying a lint rule label Mar 24, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Mar 24, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Formatter (stable)

✅ ecosystem check detected no format changes.

Formatter (preview)

✅ ecosystem check detected no format changes.

Copy link
Member

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me. I've a few smaller suggestions

Comment on lines 334 to 338
let range = if ident.range.start() > other.range.start() {
ident.range
} else {
other.range
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that we only need the range comparison here because we visit the PatternMatchAs fields not in the source order. That took me a while to realise. I think I'd prefer to, instead, change visit_pattern so that it visits all fields in source order, so that we can remove the range handling here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, I thought I had tried that first and got surprised when they didn't come out in source order, but I think I just had the order wrong in the MatchAs case.

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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is O(n^2). Should we change the visitor to first collect all names, then sort by name (log (n)), then identify duplicate names by comparing neighboring elements (n)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's closer to what I had initially, but I ran into some issues with MatchOr, where the patterns needed to be visited independently. I found a way to work around it and think this is better, thanks!

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we should mention the variable name here? It wasn't clear to me from the error message what's wrong in this example: Can I only have a single variable? It might be especially hard to realize what's wrong if the use of the same identifier isn't next to each other ({ 1: x, 2: aaaa_very_long_key, ..., 4: x })

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point. I was trying to avoid putting data into the SemanticSyntaxErrorKind variants, but we can give better error messages if we do. I'll try something more like Python:

>>> match 2:
...     case x as x: ...
...
  File "<python-input-4>", line 2
    case x as x: ...
         ^^^^^^
SyntaxError: multiple assignments to name 'x' in pattern
>>> match 2:
...     case [x, y, *x]: ...
...
  File "<python-input-5>", line 2
    case [x, y, *x]: ...
                ^^
SyntaxError: multiple assignments to name 'x' in pattern

@ntBre ntBre added the preview Related to preview mode features label Mar 24, 2025
}
}
Pattern::MatchOr(ast::PatternMatchOr { patterns, .. }) => {
// each of these patterns should be visited separately
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be useful to provide the reason for why should this be done. From what I understand, I think it's because they're separate patterns and checking for duplicate names are isolated to a single pattern.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expanded the comment and also moved the relevant test_ok case down.

I also switched to the hash set implementation we discussed yesterday and cleaned up some of the docs and code locations.

@ntBre ntBre merged commit d70a3e6 into main Mar 26, 2025
22 checks passed
@ntBre ntBre deleted the brent/multiple-assignment-case-pattern branch March 26, 2025 17:02
@sammorley-short
Copy link

sammorley-short commented Apr 4, 2025

@ntBre I am seeing an linting error thrown for case Class(x=x). This hasn't been an issue for us in our code (i.e. it compiles and runs just fine), but it's not clear from the python docs whether this should or shouldn't be allowed.

Any idea what should be happening here?

@ntBre
Copy link
Contributor Author

ntBre commented Apr 4, 2025

@sammorley-short Yeah that was a bug in this PR, first reported in #17181. It's been fixed but not released yet. 0.11.4 should come out with the fix today. Thanks for the report!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

preview Related to preview mode features rule Implementing or modifying a lint rule

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants