Skip to content

Commit ef429e0

Browse files
committed
Report irrefutable match patterns that make later cases unreachable
CPython rejects at compile time a match statement whose non-final case is an unguarded capture or wildcard, or that contains an or-pattern with an irrefutable alternative in a non-final position ('name capture 'y' makes remaining patterns unreachable'), since such patterns make the remaining cases unreachable. mypy checked such files clean. Mirror that check in the match statement checker: report captures and wildcards in non-final cases (unless guarded) and in non-final or-pattern alternatives at any nesting depth. Captures inside composite patterns (sequence, mapping, class) stay allowed, matching CPython. Unlike CPython, which stops at the first occurrence, all of them are reported. Fixes #21925
1 parent 3b28c29 commit ef429e0

3 files changed

Lines changed: 148 additions & 1 deletion

File tree

‎mypy/checker.py‎

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ def __init__(self) -> None:
211211
)
212212
from mypy.operators import flip_ops, int_op_to_method, neg_ops
213213
from mypy.options import PRECISE_TUPLE_TYPES, Options
214-
from mypy.patterns import AsPattern, StarredPattern
214+
from mypy.patterns import AsPattern, OrPattern, Pattern, StarredPattern, sub_patterns
215215
from mypy.plugin import Plugin
216216
from mypy.plugins import dataclasses as dataclasses_plugin
217217
from mypy.scope import Scope
@@ -6055,6 +6055,8 @@ def visit_continue_stmt(self, s: ContinueStmt) -> None:
60556055
return
60566056

60576057
def visit_match_stmt(self, s: MatchStmt) -> None:
6058+
if not self.current_node_deferred:
6059+
self.check_irrefutable_match_patterns(s)
60586060
# In sync with similar actions elsewhere, narrow the target if
60596061
# we are matching an AssignmentExpr
60606062
unwrapped_subject = collapse_walrus(s.subject)
@@ -6141,6 +6143,53 @@ def visit_match_stmt(self, s: MatchStmt) -> None:
61416143
with self.binder.frame_context(can_skip=False, fall_through=2):
61426144
pass
61436145

6146+
def check_irrefutable_match_patterns(self, s: MatchStmt) -> None:
6147+
"""Report capture and wildcard patterns that CPython rejects at compile time.
6148+
6149+
An unguarded capture or wildcard in a non-final case, or in a non-final
6150+
alternative of an or-pattern, makes the remaining patterns unreachable,
6151+
so CPython refuses such files with a SyntaxError (PEP 634). Mirror that
6152+
here so files that cannot even be imported don't type check clean.
6153+
"""
6154+
for i, (pattern, guard) in enumerate(zip(s.patterns, s.guards)):
6155+
# Only a final case, or a case with a guard, can be irrefutable.
6156+
allow_irrefutable = i == len(s.patterns) - 1 or guard is not None
6157+
self.check_irrefutable_pattern(pattern, allow_irrefutable)
6158+
6159+
def check_irrefutable_pattern(self, pattern: Pattern, allow_irrefutable: bool) -> None:
6160+
"""Check a pattern in a position where a capture would be irrefutable.
6161+
6162+
Captures inside composite patterns (e.g. '[x]' or 'Cls(x)') are always
6163+
allowed, matching CPython, but a nested or-pattern is checked anywhere
6164+
it appears.
6165+
"""
6166+
if isinstance(pattern, AsPattern):
6167+
if pattern.pattern is None:
6168+
# A capture pattern ('x') or a wildcard pattern ('_').
6169+
if not allow_irrefutable:
6170+
if pattern.name is not None:
6171+
self.msg.fail(
6172+
"Name capture "
6173+
f"'{pattern.name.name}' makes remaining patterns unreachable",
6174+
pattern,
6175+
)
6176+
else:
6177+
self.msg.fail("Wildcard makes remaining patterns unreachable", pattern)
6178+
return
6179+
# An as pattern is irrefutable iff its inner pattern is.
6180+
self.check_irrefutable_pattern(pattern.pattern, allow_irrefutable)
6181+
elif isinstance(pattern, OrPattern):
6182+
*alternatives, last = pattern.patterns
6183+
for alternative in alternatives:
6184+
self.check_irrefutable_pattern(alternative, False)
6185+
self.check_irrefutable_pattern(last, allow_irrefutable)
6186+
else:
6187+
for sub_pattern in sub_patterns(pattern):
6188+
# Captures in composite patterns are always allowed, so
6189+
# check sub-patterns as if they were in a final case, but
6190+
# an or-pattern alternative position still rejects them.
6191+
self.check_irrefutable_pattern(sub_pattern, True)
6192+
61446193
def _make_named_statement_for_match(self, s: MatchStmt, subject: Expression) -> Expression:
61456194
"""Construct a fake NameExpr for inference if a match clause is complex."""
61466195
if self.binder.can_put_directly(subject):

‎mypy/patterns.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,19 @@ def accept(self, visitor: PatternVisitor[T]) -> T:
160160
| MappingPattern
161161
| ClassPattern
162162
)
163+
164+
165+
def sub_patterns(pattern: Pattern) -> list[ConcretePattern]:
166+
"""Return the direct sub-patterns of a composite pattern.
167+
168+
Captures inside composite patterns (e.g. '[x]' or 'Cls(x)') are always
169+
allowed by CPython's irrefutability check, which only inspects them in
170+
top-level positions and or-pattern alternatives.
171+
"""
172+
if isinstance(pattern, SequencePattern):
173+
return pattern.patterns
174+
if isinstance(pattern, MappingPattern):
175+
return pattern.values
176+
if isinstance(pattern, ClassPattern):
177+
return [*pattern.positionals, *pattern.keyword_values]
178+
return []

‎test-data/unit/check-python310.test‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4019,3 +4019,85 @@ def enum_then_dummy_class(arg: DummyClass | Literal[MyEnum.RELEVANT]):
40194019
case _:
40204020
pass # E: Statement is unreachable
40214021
[builtins fixtures/tuple.pyi]
4022+
4023+
-- Irrefutable patterns making remaining cases unreachable --
4024+
4025+
[case testMatchIrrefutableCaptureNotLast]
4026+
def f(x: int) -> None:
4027+
match x:
4028+
case y: # E: Name capture 'y' makes remaining patterns unreachable
4029+
pass
4030+
case _:
4031+
pass
4032+
4033+
[case testMatchIrrefutableWildcardNotLast]
4034+
def f(x: int) -> None:
4035+
match x:
4036+
case _: # E: Wildcard makes remaining patterns unreachable
4037+
pass
4038+
case 1:
4039+
pass
4040+
4041+
[case testMatchIrrefutableCaptureWithGuard]
4042+
def f(x: int, cond: bool) -> None:
4043+
match x:
4044+
case y if cond:
4045+
pass
4046+
case _:
4047+
pass
4048+
4049+
[case testMatchIrrefutableLastCase]
4050+
def f(x: int) -> None:
4051+
match x:
4052+
case 1:
4053+
pass
4054+
case y:
4055+
pass
4056+
4057+
[case testMatchIrrefutableOrPatternNotLast]
4058+
def f(x: int) -> None:
4059+
match x:
4060+
case _ | 1: # E: Wildcard makes remaining patterns unreachable
4061+
pass
4062+
case 2:
4063+
pass
4064+
4065+
[case testMatchIrrefutableOrPatternLast]
4066+
def f(x: int) -> None:
4067+
match x:
4068+
case 1 | _:
4069+
pass
4070+
4071+
[case testMatchIrrefutableNestedOrPattern]
4072+
def f(x: int) -> None:
4073+
match x:
4074+
case [1, _ | 2]: # E: Wildcard makes remaining patterns unreachable
4075+
pass
4076+
case _:
4077+
pass
4078+
4079+
[case testMatchIrrefutableCompositeCaptureAllowed]
4080+
def f(x: int) -> None:
4081+
match x:
4082+
case [y]:
4083+
pass
4084+
case _:
4085+
pass
4086+
4087+
[case testMatchIrrefutableCaptureAs]
4088+
def f(x: int) -> None:
4089+
match x:
4090+
case y as z: # E: Name capture 'y' makes remaining patterns unreachable
4091+
pass
4092+
case _:
4093+
pass
4094+
4095+
[case testMatchIrrefutableTwoCaptures]
4096+
def f(x: int) -> None:
4097+
match x:
4098+
case y: # E: Name capture 'y' makes remaining patterns unreachable
4099+
pass
4100+
case z: # E: Name capture 'z' makes remaining patterns unreachable
4101+
pass
4102+
case _:
4103+
pass

0 commit comments

Comments
 (0)