Repository: wemake-services/wemake-python-styleguide

Issue:
Find identical `case` statements in `WPS533`
I forgot about it when provided initial implementation.
Or maybe create a separate violation?

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/wemake-services/wemake-python-styleguide/pull/3208?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wemake-services) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 100.00%. Comparing base [(`e878f3a`)](https://app.codecov.io/gh/wemake-services/wemake-python-styleguide/commit/e878f3a0cd17c612ad1bc4b442bdcd1c005d20c2?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wemake-services) to head [(`4e6bbfe`)](https://app.codecov.io/gh/wemake-services/wemake-python-styleguide/commit/4e6bbfe4ac7529fe4f5f4b76edc09839116c54ff?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wemake-services).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##            master     #3208   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          376       377    +1     
  Lines        12749     12753    +4     
  Branches       875       870    -5     
=========================================
+ Hits         12749     12753    +4     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/wemake-services/wemake-python-styleguide/pull/3208?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wemake-services).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wemake-services).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6e6fb5b6..f7bf028f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -96,6 +96,7 @@ Semantic versioning in our case means:
 - Adds a new rule to forbid `lambda` assigns to special attributes, #1733
 - Adds a new rule to check problematic function params, #1343
 - Adds a new rule to detect duplicate conditions in `if`s and `elif`s, #2241
+- Adds a new rule to detect duplicate `case` pattens in `match`, #3206
 - Adds a new rule to find too complex `except` with too many exceptions
 - Adds a new rule to find too many `PEP695` type params
 - Adds a new rule to find useless ternary expressions, #1706
diff --git a/wemake_python_styleguide/presets/types/tree.py b/wemake_python_styleguide/presets/types/tree.py
index 3f88f4cd..4a0863bd 100644
--- a/wemake_python_styleguide/presets/types/tree.py
+++ b/wemake_python_styleguide/presets/types/tree.py
@@ -71,6 +71,7 @@ PRESET: Final = (
     conditions.IfStatementVisitor,
     conditions.BooleanConditionVisitor,
     conditions.ImplicitBoolPatternsVisitor,
+    conditions.MatchVisitor,
     conditions.ChainedIsVisitor,
     iterables.IterableUnpackingVisitor,
     classes.WrongClassDefVisitor,
diff --git a/wemake_python_styleguide/violations/refactoring.py b/wemake_python_styleguide/violations/refactoring.py
index f327936c..37a4de32 100644
--- a/wemake_python_styleguide/violations/refactoring.py
+++ b/wemake_python_styleguide/violations/refactoring.py
@@ -48,6 +48,7 @@ Summary
    ChainedIsViolation
    DuplicateIfConditionViolation
    UselessTernaryViolation
+   DuplicateCasePatternViolation
 
 Refactoring opportunities
 -------------------------
@@ -87,6 +88,7 @@ Refactoring opportunities
 .. autoclass:: ChainedIsViolation
 .. autoclass:: DuplicateIfConditionViolation
 .. autoclass:: UselessTernaryViolation
+.. autoclass:: DuplicateCasePatternViolation
 
 """
 
@@ -1358,3 +1360,35 @@ class UselessTernaryViolation(ASTViolation):
 
     error_template = 'Found useless ternary expression'
     code = 534
+
+
+@final
+class DuplicateCasePatternViolation(ASTViolation):
+    """
+    Forbid having duplicate ``case`` patterns.
+
+    Reasoning:
+        It is likely an error to have multiple same int ``case`` patterns.
+        Only the first one will always work.
+
+    Solution:
+        Change the pattern.
+
+    Example::
+
+        # Correct:
+        match some:
+            case SomeClass(field) if field > 0: ...
+            case OtherClass(): ...
+
+        # Wrong:
+        match some:
+            case SomeClass(field): ...
+            case SomeClass(field): ...
+
+    .. versionadded:: 1.0.0
+
+    """
+
+    error_template = 'Found duplicate `case` pattern: {0}'
+    code = 535
diff --git a/wemake_python_styleguide/visitors/ast/conditions.py b/wemake_python_styleguide/visitors/ast/conditions.py
index f71781d1..1c5fa2a7 100644
--- a/wemake_python_styleguide/visitors/ast/conditions.py
+++ b/wemake_python_styleguide/visitors/ast/conditions.py
@@ -206,6 +206,32 @@ class ImplicitBoolPatternsVisitor(BaseNodeVisitor):
             )
 
 
+@final
+class MatchVisitor(BaseNodeVisitor):
+    """Visits conditions in pattern matching."""
+
+    def visit_Match(self, node: ast.Match) -> None:
+        """Finds issues in PM conditions."""
+        self._check_duplicate_cases(node)
+        self.generic_visit(node)
+
+    def _check_duplicate_cases(self, node: ast.Match) -> None:
+        conditions = [self._parse_case(case_node) for case_node in node.cases]
+        for condition, times in Counter(conditions).items():
+            if times > 1:
+                self.add_violation(
+                    refactoring.DuplicateCasePatternViolation(
+                        node,
+                        text=condition,
+                    )
+                )
+
+    def _parse_case(self, node: ast.match_case) -> str:
+        pattern = source.node_to_string(node.pattern)
+        guard = source.node_to_string(node.guard) if node.guard else ''
+        return f'{pattern} if {guard}' if guard else pattern
+
+
 @final
 class ChainedIsVisitor(BaseNodeVisitor):
     """Is used to find chained `is` comparisons."""


Test patch:
diff --git a/tests/fixtures/noqa/noqa.py b/tests/fixtures/noqa/noqa.py
index 41607b22..a67734bf 100644
--- a/tests/fixtures/noqa/noqa.py
+++ b/tests/fixtures/noqa/noqa.py
@@ -677,6 +677,12 @@ elif noqa_wps533:
 
 noqa_wps534 = second if first == second else first  # noqa: WPS534
 
+match some_value:  # noqa: WPS535
+    case SomeClass():
+        my_print('first')
+    case SomeClass():
+        my_print('second')
+
 class Baseline:
     def method(self, number):
         return number + 1
diff --git a/tests/test_checker/test_noqa.py b/tests/test_checker/test_noqa.py
index 7f0863cd..638bd73d 100644
--- a/tests/test_checker/test_noqa.py
+++ b/tests/test_checker/test_noqa.py
@@ -267,6 +267,7 @@ SHOULD_BE_RAISED = types.MappingProxyType(
         'WPS532': 1,
         'WPS533': 1,
         'WPS534': 1,
+        'WPS535': 1,
         'WPS600': 1,
         'WPS601': 1,
         'WPS602': 2,
diff --git a/tests/test_visitors/test_ast/test_conditions/test_duplicated_case_patterns.py b/tests/test_visitors/test_ast/test_conditions/test_duplicated_case_patterns.py
new file mode 100644
index 00000000..cbba4abc
--- /dev/null
+++ b/tests/test_visitors/test_ast/test_conditions/test_duplicated_case_patterns.py
@@ -0,0 +1,174 @@
+import pytest
+
+from wemake_python_styleguide.violations.refactoring import (
+    DuplicateCasePatternViolation,
+)
+from wemake_python_styleguide.visitors.ast.conditions import MatchVisitor
+
+# Always correct:
+
+match_single = """
+match some_value:
+    case {0}:
+        ...
+"""
+
+match_after_match = """
+match some_value:
+    case {0}:
+        ...
+match some_value:
+    case {0}:
+        ...
+"""
+
+# Can be duplicated:
+
+match_with_two_cases1 = """
+match some_value:
+    case {0}:
+        ...
+    case {1}:
+        ...
+"""
+
+match_with_two_cases2 = """
+match some_value:
+    case {0}:
+        ...
+    case will_be_hard_to_match():
+        ...
+    case {1}:
+        ...
+"""
+
+# Duplicating twice should raise twice:
+match_two_dups = """
+match some_value:
+    case SomeClass(field) if field > 0: ...
+    case SomeClass(field) if field > 0: ...
+    case OtherClass(): ...
+    case OtherClass(): ...
+"""
+
+# List of conditions to test:
+conditions = [
+    'SomeClass(field)',
+    'SomeClass(field) if field > 0',
+    'SomeClass(field) as named if field > 0',
+    '[some]',
+    '{}',
+    "{'key': some} if check(some)",
+    'FirstClass(x) | [x]',
+    "['first', [x] as lst]",
+]
+
+
+@pytest.mark.parametrize(
+    'template',
+    [
+        match_single,
+        match_after_match,
+    ],
+)
+@pytest.mark.parametrize(
+    'code',
+    conditions,
+)
+def test_always_correct_conditions(
+    template,
+    code,
+    assert_errors,
+    parse_ast_tree,
+    default_options,
+):
+    """Testing simple conditions."""
+    tree = parse_ast_tree(template.format(code))
+
+    visitor = MatchVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])
+
+
+@pytest.mark.parametrize(
+    'template',
+    [
+        match_with_two_cases1,
+        match_with_two_cases2,
+    ],
+)
+@pytest.mark.parametrize(
+    'code',
+    conditions,
+)
+def test_different_conditions(
+    template,
+    code,
+    assert_errors,
+    parse_ast_tree,
+    default_options,
+):
+    """Testing that different conditions produce no errors."""
+    tree = parse_ast_tree(template.format(code, 'some_test_other_value'))
+
+    visitor = MatchVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])
+
+
+@pytest.mark.parametrize(
+    'template',
+    [
+        match_with_two_cases1,
+        match_with_two_cases2,
+    ],
+)
+@pytest.mark.parametrize(
+    'code',
+    conditions,
+)
+def test_same_conditions(
+    template,
+    code,
+    assert_errors,
+    assert_error_text,
+    parse_ast_tree,
+    default_options,
+):
+    """Testing that same conditions trigger a violation."""
+    tree = parse_ast_tree(template.format(code, code))
+
+    visitor = MatchVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(
+        visitor,
+        [DuplicateCasePatternViolation],
+    )
+    assert_error_text(visitor, code)
+
+
+@pytest.mark.parametrize(
+    'code',
+    [
+        match_two_dups,
+    ],
+)
+def test_two_duplicate_conditions(
+    code,
+    assert_errors,
+    parse_ast_tree,
+    default_options,
+):
+    """Testing that same conditions trigger a violation."""
+    tree = parse_ast_tree(code)
+
+    visitor = MatchVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(
+        visitor,
+        [DuplicateCasePatternViolation, DuplicateCasePatternViolation],
+    )