diff --git a/mypy/message_registry.py b/mypy/message_registry.py index 2e40048cbb58..9cea9e8383bc 100644 --- a/mypy/message_registry.py +++ b/mypy/message_registry.py @@ -281,6 +281,12 @@ def with_additional_msg(self, info: str) -> ErrorMessage: # Match Statement MISSING_MATCH_ARGS: Final = 'Class "{}" doesn\'t define "__match_args__"' OR_PATTERN_ALTERNATIVE_NAMES: Final = "Alternative patterns bind different names" +NAME_CAPTURE_MAKES_REMAINING_UNREACHABLE: Final = ( + 'Name capture "{}" makes remaining patterns unreachable' +) +WILDCARD_MAKES_REMAINING_UNREACHABLE: Final = ( + "Wildcard pattern makes remaining patterns unreachable" +) CLASS_PATTERN_GENERIC_TYPE_ALIAS: Final = ( "Class pattern class must not be a type alias with type parameters" ) diff --git a/mypy/patterns.py b/mypy/patterns.py index a01bf6acc876..2917a5c6e5d9 100644 --- a/mypy/patterns.py +++ b/mypy/patterns.py @@ -148,3 +148,19 @@ def __init__( def accept(self, visitor: PatternVisitor[T]) -> T: return visitor.visit_class_pattern(self) + + +def get_irrefutable_pattern(pattern: Pattern) -> AsPattern | None: + """Return the capture or wildcard pattern that makes this pattern irrefutable. + + An irrefutable pattern matches any subject: a capture pattern, a wildcard + pattern, an as pattern whose subpattern is irrefutable, or an or pattern + whose last alternative is irrefutable. Returns None for refutable patterns. + """ + if isinstance(pattern, AsPattern): + if pattern.pattern is None: + return pattern + return get_irrefutable_pattern(pattern.pattern) + if isinstance(pattern, OrPattern): + return get_irrefutable_pattern(pattern.patterns[-1]) + return None diff --git a/mypy/semanal.py b/mypy/semanal.py index cd1b0a738974..86d4abf8d3de 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -207,6 +207,7 @@ SingletonPattern, StarredPattern, ValuePattern, + get_irrefutable_pattern, ) from mypy.plugin import ( ClassDefContext, @@ -5785,12 +5786,28 @@ def visit_match_stmt(self, s: MatchStmt) -> None: infer_reachability_of_match_statement(s, self.options) s.subject.accept(self) for i in range(len(s.patterns)): + # An unguarded irrefutable pattern is only allowed in the last case, + # otherwise the remaining cases could never match. CPython rejects + # such match statements at compile time with a SyntaxError. + if i < len(s.patterns) - 1 and s.guards[i] is None: + irrefutable = get_irrefutable_pattern(s.patterns[i]) + if irrefutable is not None: + self.fail_irrefutable_pattern(irrefutable) s.patterns[i].accept(self) guard = s.guards[i] if guard is not None: guard.accept(self) self.visit_block(s.bodies[i]) + def fail_irrefutable_pattern(self, pattern: AsPattern) -> None: + if pattern.name is None: + msg = message_registry.WILDCARD_MAKES_REMAINING_UNREACHABLE + else: + msg = message_registry.NAME_CAPTURE_MAKES_REMAINING_UNREACHABLE.format( + pattern.name.name + ) + self.fail(msg, pattern, serious=True) + def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: if s.invalid_recursive_alias: return @@ -6555,6 +6572,13 @@ def visit_as_pattern(self, p: AsPattern) -> None: self.analyze_lvalue(p.name) def visit_or_pattern(self, p: OrPattern) -> None: + # An irrefutable alternative is only allowed in the last position, + # regardless of where the or pattern appears. CPython rejects other + # placements at compile time with a SyntaxError. + for pattern in p.patterns[:-1]: + irrefutable = get_irrefutable_pattern(pattern) + if irrefutable is not None: + self.fail_irrefutable_pattern(irrefutable) for pattern in p.patterns: pattern.accept(self) diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 01e490d0da50..1ef9a3c98500 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -4005,3 +4005,120 @@ def enum_then_dummy_class(arg: DummyClass | Literal[MyEnum.RELEVANT]): case _: pass # E: Statement is unreachable [builtins fixtures/tuple.pyi] + + +[case testMatchIrrefutablePatternNotLastCase] +# Irrefutable patterns before the last case are a SyntaxError at runtime +def capture(x: int) -> None: + match x: + case y: # E: Name capture "y" makes remaining patterns unreachable + pass + case 1: + pass + +def wildcard(x: int) -> None: + match x: + case _: # E: Wildcard pattern makes remaining patterns unreachable + pass + case 1: + pass + +def as_capture(x: int) -> None: + match x: + case (y as z): # E: Name capture "y" makes remaining patterns unreachable + pass + case 1: + pass + +def or_ending_irrefutable(x: int) -> None: + match x: + case 1 | _: # E: Wildcard pattern makes remaining patterns unreachable + pass + case 2: + pass +[builtins fixtures/tuple.pyi] + +[case testMatchIrrefutablePatternAllowed] +def capture_last(x: int) -> None: + match x: + case 1: + pass + case y: + pass + +def wildcard_last(x: int) -> None: + match x: + case 1: + pass + case _: + pass + +def check(v: int) -> bool: ... + +def guarded_capture(x: int) -> None: + match x: + case y if check(y): + pass + case _: + pass + +def guarded_wildcard(x: int) -> None: + match x: + case _ if check(x): + pass + case 1: + pass + +def matches_anything_but_refutable(x: int) -> None: + match x: + case int(): + pass + case _: + pass +[builtins fixtures/tuple.pyi] + +[case testMatchIrrefutableOrPatternAlternativeNotLast] +# An irrefutable or pattern alternative is only allowed in the last position, +# even in the last case, in a guarded case, or nested in another pattern +def wildcard_alternative_last_case(x: int) -> None: + match x: + case 1: + pass + case _ | 2: # E: Wildcard pattern makes remaining patterns unreachable + pass + +def check(v: int) -> bool: ... + +def wildcard_alternative_guarded(x: int) -> None: + match x: + case _ | 1 if check(x): # E: Wildcard pattern makes remaining patterns unreachable + pass + case 2: + pass + +def nested_in_sequence(x: object) -> None: + match x: + case [_ | 1, y]: # E: Wildcard pattern makes remaining patterns unreachable + pass + +def nested_in_or(x: int) -> None: + match x: + case 1 | (2 | _) | 3: # E: Wildcard pattern makes remaining patterns unreachable + pass + +def or_ending_irrefutable_last_case(x: int) -> None: + match x: + case 1: + pass + case 2 | _: + pass +[builtins fixtures/tuple.pyi] + +[case testMatchIrrefutablePatternUncheckedFunction] +def f(x): + match x: + case y: # E: Name capture "y" makes remaining patterns unreachable + pass + case 1: + pass +[builtins fixtures/tuple.pyi]