diff --git a/mypy/checker.py b/mypy/checker.py index 33ed5387554d..cf99be9d1f13 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -211,7 +211,7 @@ def __init__(self) -> None: ) from mypy.operators import flip_ops, int_op_to_method, neg_ops from mypy.options import PRECISE_TUPLE_TYPES, Options -from mypy.patterns import AsPattern, StarredPattern +from mypy.patterns import AsPattern, OrPattern, Pattern, StarredPattern, sub_patterns from mypy.plugin import Plugin from mypy.plugins import dataclasses as dataclasses_plugin from mypy.scope import Scope @@ -6014,6 +6014,8 @@ def visit_continue_stmt(self, s: ContinueStmt) -> None: return def visit_match_stmt(self, s: MatchStmt) -> None: + if not self.current_node_deferred: + self.check_irrefutable_match_patterns(s) # In sync with similar actions elsewhere, narrow the target if # we are matching an AssignmentExpr unwrapped_subject = collapse_walrus(s.subject) @@ -6100,6 +6102,53 @@ def visit_match_stmt(self, s: MatchStmt) -> None: with self.binder.frame_context(can_skip=False, fall_through=2): pass + def check_irrefutable_match_patterns(self, s: MatchStmt) -> None: + """Report capture and wildcard patterns that CPython rejects at compile time. + + An unguarded capture or wildcard in a non-final case, or in a non-final + alternative of an or-pattern, makes the remaining patterns unreachable, + so CPython refuses such files with a SyntaxError (PEP 634). Mirror that + here so files that cannot even be imported don't type check clean. + """ + for i, (pattern, guard) in enumerate(zip(s.patterns, s.guards)): + # Only a final case, or a case with a guard, can be irrefutable. + allow_irrefutable = i == len(s.patterns) - 1 or guard is not None + self.check_irrefutable_pattern(pattern, allow_irrefutable) + + def check_irrefutable_pattern(self, pattern: Pattern, allow_irrefutable: bool) -> None: + """Check a pattern in a position where a capture would be irrefutable. + + Captures inside composite patterns (e.g. '[x]' or 'Cls(x)') are always + allowed, matching CPython, but a nested or-pattern is checked anywhere + it appears. + """ + if isinstance(pattern, AsPattern): + if pattern.pattern is None: + # A capture pattern ('x') or a wildcard pattern ('_'). + if not allow_irrefutable: + if pattern.name is not None: + self.msg.fail( + "Name capture " + f"'{pattern.name.name}' makes remaining patterns unreachable", + pattern, + ) + else: + self.msg.fail("Wildcard makes remaining patterns unreachable", pattern) + return + # An as pattern is irrefutable iff its inner pattern is. + self.check_irrefutable_pattern(pattern.pattern, allow_irrefutable) + elif isinstance(pattern, OrPattern): + *alternatives, last = pattern.patterns + for alternative in alternatives: + self.check_irrefutable_pattern(alternative, False) + self.check_irrefutable_pattern(last, allow_irrefutable) + else: + for sub_pattern in sub_patterns(pattern): + # Captures in composite patterns are always allowed, so + # check sub-patterns as if they were in a final case, but + # an or-pattern alternative position still rejects them. + self.check_irrefutable_pattern(sub_pattern, True) + def _make_named_statement_for_match(self, s: MatchStmt, subject: Expression) -> Expression: """Construct a fake NameExpr for inference if a match clause is complex.""" if self.binder.can_put_directly(subject): diff --git a/mypy/patterns.py b/mypy/patterns.py index a01bf6acc876..5bd6a96dc3ee 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 sub_patterns(pattern: Pattern) -> list[Pattern]: + """Return the direct sub-patterns of a composite pattern. + + Captures inside composite patterns (e.g. '[x]' or 'Cls(x)') are always + allowed by CPython's irrefutability check, which only inspects them in + top-level positions and or-pattern alternatives. + """ + if isinstance(pattern, SequencePattern): + return pattern.patterns + if isinstance(pattern, MappingPattern): + return pattern.values + if isinstance(pattern, ClassPattern): + return [*pattern.positionals, *pattern.keyword_values] + return [] diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 01e490d0da50..af51302ef272 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -4005,3 +4005,85 @@ def enum_then_dummy_class(arg: DummyClass | Literal[MyEnum.RELEVANT]): case _: pass # E: Statement is unreachable [builtins fixtures/tuple.pyi] + +-- Irrefutable patterns making remaining cases unreachable -- + +[case testMatchIrrefutableCaptureNotLast] +def f(x: int) -> None: + match x: + case y: # E: Name capture 'y' makes remaining patterns unreachable + pass + case _: + pass + +[case testMatchIrrefutableWildcardNotLast] +def f(x: int) -> None: + match x: + case _: # E: Wildcard makes remaining patterns unreachable + pass + case 1: + pass + +[case testMatchIrrefutableCaptureWithGuard] +def f(x: int, cond: bool) -> None: + match x: + case y if cond: + pass + case _: + pass + +[case testMatchIrrefutableLastCase] +def f(x: int) -> None: + match x: + case 1: + pass + case y: + pass + +[case testMatchIrrefutableOrPatternNotLast] +def f(x: int) -> None: + match x: + case _ | 1: # E: Wildcard makes remaining patterns unreachable + pass + case 2: + pass + +[case testMatchIrrefutableOrPatternLast] +def f(x: int) -> None: + match x: + case 1 | _: + pass + +[case testMatchIrrefutableNestedOrPattern] +def f(x: int) -> None: + match x: + case [1, _ | 2]: # E: Wildcard makes remaining patterns unreachable + pass + case _: + pass + +[case testMatchIrrefutableCompositeCaptureAllowed] +def f(x: int) -> None: + match x: + case [y]: + pass + case _: + pass + +[case testMatchIrrefutableCaptureAs] +def f(x: int) -> None: + match x: + case y as z: # E: Name capture 'y' makes remaining patterns unreachable + pass + case _: + pass + +[case testMatchIrrefutableTwoCaptures] +def f(x: int) -> None: + match x: + case y: # E: Name capture 'y' makes remaining patterns unreachable + pass + case z: # E: Name capture 'z' makes remaining patterns unreachable + pass + case _: + pass